/// <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);
                }
            }
        }