/// <summary> /// Binds an event handler to a property on an <see cref="INotifyPropertyChanged"/>. Updates the attachedness of the handler when the property changes. /// </summary> /// <typeparam name="TEventArgs"></typeparam> /// <param name="source"> The object that has the property to be monitored. </param> /// <param name="propertyName"> The name of the property on the source. </param> /// <param name="eventName"> The name of the event on the property on the source. </param> /// <param name="eventHandler"> The handler to be invoked when the event on the current property value fires. </param> public static void BindEvent <TEventArgs>(this INotifyPropertyChanged source, string propertyName, string eventName, Action <object, TEventArgs> eventHandler) where TEventArgs : EventArgs { Contract.Requires(source != null); Contract.Requires(!string.IsNullOrEmpty(eventName)); var propertyType = GetPropertyType(source.GetType(), propertyName); Contract.Requires(propertyType != null); var eventInfo = propertyType.GetEvent(eventName); Contract.Requires(eventInfo != null); Contract.Requires(eventInfo.HasHandlerSignature <TEventArgs>()); Contract.Requires(eventHandler != null); source.PropertyChanged += sourcePropertyChanged; sourcePropertyChanged(source, PropertyMutatedEventArgsExtensions.From(source, propertyName)); void sourcePropertyChanged(object sender, PropertyChangedEventArgs e_) { if (e_.PropertyName == propertyName) { var e = (IPropertyMutatedEventArgs)e_; if (e.OldValue != null) { eventInfo.RemoveEventHandler(e.OldValue, eventHandler); } if (e.NewValue != null) { eventInfo.AddEventHandler(e.NewValue, eventHandler); } } } }
/// <summary> /// Binds a <see cref="NotifyCollectionChangedEventHandler"/> to all collections by the name <paramref name="collectionPropertyName"/> on the elements of type <typeparamref name="T"/> on the specified collection. /// Note: not all elements of <paramref name="collection"/> are considered; only those of type <typeparamref name="T"/>. You could specify <see cref="INotifyPropertyChanged"/> to revert this. /// </summary> /// <returns> a disposble which unhooks the <paramref name="handler"/> from all elements. </returns> public static IDisposable BindElements <TCollection, T>( this TCollection collection, string collectionPropertyName, NotifyCollectionChangedEventHandler handler) where T : INotifyPropertyChanged where TCollection : INotifyCollectionChanged, IEnumerable <object> { Contract.Requires(collection != null); Contract.Requires(handler != null); PropertyChangedEventHandler intermediate = (element, e) => { if (e.PropertyName == collectionPropertyName) { var _e = (IPropertyMutatedEventArgs)e; if (_e.OldValue is INotifyCollectionChanged oldCollection) { oldCollection.CollectionChanged -= handler; } if (_e.NewValue is INotifyCollectionChanged newCollection) { newCollection.CollectionChanged += handler; } } }; var collectionHandler = CollectionChangedEventHandler <object> .Create <TCollection>(add : add, remove : remove); collection.CollectionChanged += collectionHandler.CollectionChanged; return(new Disposable(() => { collection.CollectionChanged -= collectionHandler.CollectionChanged; foreach (T element in collection) { element.PropertyChanged -= intermediate; } })); void add(TCollection sender, IReadOnlyList <object> newElements, int index) { Contract.Assert(ReferenceEquals(collection, sender)); foreach (T element in newElements.OfType <T>()) { element.PropertyChanged += intermediate; intermediate(element, PropertyMutatedEventArgsExtensions.From(element, collectionPropertyName)); } } void remove(TCollection sender, IReadOnlyList <object> oldElements, int index) { Contract.Assert(ReferenceEquals(collection, sender)); foreach (T element in oldElements.OfType <T>()) { element.PropertyChanged -= intermediate; } } }
/// <summary> Implements the functionality documented in the other overloads. </summary> private static void Bind(INotifyPropertyChanged source, string propertyName, string propertyOnPropertyName, PropertyChangedEventHandler handler, object defaultValue, Type propertyOnPropertyType) { Contract.Requires(source != null); Contract.Requires(handler != null); Contract.Requires(source.GetType().GetProperty(propertyName) != null); var sourceProperty = GetProperty(source.GetType(), propertyName); var sourcePropertyType = GetPropertyType(source.GetType(), propertyName); Contract.Requires(sourcePropertyType.Implements(typeof(INotifyPropertyChanged)), $"The property on the source must implement '{nameof(INotifyPropertyChanged)}'"); Contract.Requires(GetPropertyType(sourcePropertyType, propertyOnPropertyName) != null, $"Property '{propertyName}' was not found on type '{sourcePropertyType.Name}'"); Contract.Requires(propertyOnPropertyType.IsAssignableFrom(GetPropertyType(sourcePropertyType, propertyOnPropertyName))); Contract.Requires(propertyOnPropertyType.IsInstanceOfType(defaultValue) || (ReferenceEquals(defaultValue, null) && !propertyOnPropertyType.IsValueType)); handler = handler.FilterOn(propertyOnPropertyName); PropertyChangedEventHandler onSourcePropertyChanged = (sender, eventArg) => { Contract.Requires(eventArg is IPropertyMutatedEventArgs); var e = (IPropertyMutatedEventArgs)eventArg; var oldSource = (INotifyPropertyChanged)e.OldValue; if (oldSource != null) { oldSource.PropertyChanged -= handler; } var newSource = (INotifyPropertyChanged)e.NewValue; if (newSource != null) { newSource.PropertyChanged += handler; } var oldValue = GetPropertyValue(oldSource, propertyOnPropertyName, defaultValue: defaultValue); var newValue = GetPropertyValue(newSource, propertyOnPropertyName, defaultValue: defaultValue); if (!Equals(oldValue, newValue)) // uses virtual method oldValue.Equals(object) { handler(newSource ?? oldSource, PropertyMutatedEventArgsExtensions.Create(propertyOnPropertyName, propertyOnPropertyType, oldValue, newValue)); } }; source.PropertyChanged += onSourcePropertyChanged.FilterOn(propertyName); object currentValue = sourceProperty.GetValue(source); // onSourcePropertyChanged is invoked regardless of whether currentValue equals sourcePropertyType.GetDefault() // because it hooks the handler onto currentValue.PropertyChange onSourcePropertyChanged(source, PropertyMutatedEventArgsExtensions.Create(propertyName, sourcePropertyType, sourcePropertyType.GetDefault(), currentValue)); }
/// <summary> /// Invokes the specified handler for each change of the specified property on any of the elements in the specified collection. /// Note that the handler is not invoked for elements already present, nor if items are added where the property already has a value. /// </summary> /// <param name="collection"> The collection of elements on which to monitor property changes. </param> /// <param name="handler"> The action invoked when the property changes. </param> /// <param name="propertyName"> The name of the property to monitor. </param> /// <param name="handleNewItems"> Indicates whether the relevant property on an item added to the collection should be treated as though it changed. </param> public static void Bind(this INotifyCollectionChanged collection, string propertyName, PropertyChangedEventHandler handler, Type elementType = null, bool handleNewItems = false, PropertyChangedEventHandler onRemove = null) { Contract.Requires(collection != null); Contract.Requires(collection is System.Collections.IEnumerable); Contract.Requires(elementType == null || HasProperty(propertyName, elementType)); collection.CollectionChanged += collectionChanged; var collectionList = collection as System.Collections.IList ?? ((System.Collections.IEnumerable)collection).Cast <object>().ToList(); if (collectionList.Count != 0) { elementType = elementType ?? collectionList[0].GetType(); Contract.Requires(HasProperty(propertyName, elementType)); collectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collectionList)); } void collectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Move) { return; } if (e.NewItems != null) { foreach (var newItem in e.NewItems) { Contract.Requires(newItem is INotifyPropertyChanged, "This binding requires that all elements be non-null and implement INotifyPropertyChanged"); ((INotifyPropertyChanged)newItem).PropertyChanged += propertyChanged; if (handleNewItems) { handler(newItem, PropertyMutatedEventArgsExtensions.From(newItem, propertyName, elementType)); } } } if (e.OldItems != null) { foreach (var newItem in e.OldItems) { ((INotifyPropertyChanged)newItem).PropertyChanged -= propertyChanged; onRemove?.Invoke(newItem, PropertyMutatedEventArgsExtensions.From(newItem, propertyName, GetPropertyValue(newItem, propertyName, elementType), newValue: null, elementType)); } } } void propertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == propertyName) { handler(sender, e); } } }