public static IDisposable connect_item_hooks <T>(ObservableCollection <T> source, Func <T, IDisposable> get_item_hook, bool clear_source_on_dispose = false)
        {
            var item_hooks = source.Select(get_item_hook).ToList();

            return(new CompositeDisposable(
                       clear_source_on_dispose ? Disposable.Create(source.Clear) : Disposable.wrap_collection(item_hooks),
                       source.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source, get_item_hook, d => d.Dispose(), e))));
        }
        public static IDisposable connect_item_hooks <T>(IReadOnlyObservableCollection <T> source, Func <T, IDisposable> get_item_hook)
        {
            var item_hooks = source.Select(get_item_hook).ToList();

            return(new CompositeDisposable(
                       Disposable.wrap_collection(item_hooks),
                       source.Subscribe((s, e) => CollectionHelper.reflect_change(item_hooks, source, get_item_hook, d => d.Dispose(), e))));
        }
        /// <summary>
        /// Establishes a 2-way connection between a target collection and a source collection.
        /// First, the target is mutated to contain the exact same elements as the source.
        /// After that, subsequent changes to either collection are replicated in the other.
        /// </summary>
        public static IDisposable connect_two_way <T>(IObservable <ObservableCollection <T> > source, ObservableCollection <T> target)
        {
            IDisposable curr_connection = null;
            IDisposable source_sub      = source.Subscribe(c => {
                Disposable.dispose(ref curr_connection);
                curr_connection = connect_two_way(c, target);
            });

            return(Disposable.Create(() => {
                Disposable.dispose(ref source_sub);
                Disposable.dispose(ref curr_connection);
            }));
        }
        /// <summary>
        /// Establishes a 2-way connection between a target collection and a source collection.
        /// First, the target is mutated to contain the exact same elements as the source.
        /// After that, subsequent changes to either collection are replicated in the other.
        /// </summary>
        public static IDisposable connect_two_way <T>(ObservableCollection <T> source, ObservableCollection <T> target)
        {
            target.SyncWith(source);
            source.CollectionChanged += source_changed;
            target.CollectionChanged += target_changed;
            return(Disposable.Create(() => {
                source.CollectionChanged -= source_changed;
                target.CollectionChanged -= target_changed;
            }));

            void source_changed(object sender, NotifyCollectionChangedEventArgs e)
            {
                using (target.SuspendHandler(target_changed))
                    CollectionHelper.reflect_change(target, source, _ => _, null, e);
            }

            void target_changed(object sender, NotifyCollectionChangedEventArgs e)
            {
                using (source.SuspendHandler(source_changed))
                    CollectionHelper.reflect_change(target, source, _ => _, null, e);
            }
        }
        public static IDisposable connect <TCollection, T, TResult>(TCollection source, IList <TResult> target, Func <T, TResult> create, Action <TResult> on_removed = null)
            where TCollection : IReadOnlyList <T>, INotifyCollectionChanged
        {
            on_collection_changed(source, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)source));
            source.CollectionChanged += on_collection_changed;
            return(Disposable.Create(() => source.CollectionChanged -= on_collection_changed));

            void on_collection_changed(object sender, NotifyCollectionChangedEventArgs e)
            {
                switch (e.Action)
                {
                case NotifyCollectionChangedAction.Add:
                    if (e.NewStartingIndex < 0)
                    {
                        for (int i = 0; i < e.NewItems.Count; i++)
                        {
                            target.Add(create((T)e.NewItems[i]));
                        }
                    }
                    else
                    {
                        for (int i = 0; i < e.NewItems.Count; i++)
                        {
                            target.Insert(e.NewStartingIndex + i, create((T)e.NewItems[i]));
                        }
                    }
                    break;

                case NotifyCollectionChangedAction.Remove:
                    validate_old_index();
                    for (int i = 0; i < e.OldItems.Count; i++)
                    {
                        on_removed?.Invoke(target[e.OldStartingIndex]);
                        target.RemoveAt(e.OldStartingIndex);
                    }
                    break;

                case NotifyCollectionChangedAction.Replace:
                    validate_old_index();
                    // remove
                    for (int i = 0; i < e.OldItems.Count; i++)
                    {
                        on_removed?.Invoke(target[e.OldStartingIndex]);
                        target.RemoveAt(e.OldStartingIndex);
                    }
                    // add
                    if (e.NewStartingIndex != e.OldStartingIndex)
                    {
                        throw new InvalidOperationException(fmt_error($"{nameof(e.OldStartingIndex)} is not equal to {nameof(e.NewStartingIndex)}"));
                    }
                    if (e.NewStartingIndex < 0)
                    {
                        for (int i = 0; i < e.NewItems.Count; i++)
                        {
                            target.Add(create((T)e.NewItems[i]));
                        }
                    }
                    else
                    {
                        for (int i = 0; i < e.NewItems.Count; i++)
                        {
                            target.Insert(e.NewStartingIndex + i, create((T)e.NewItems[i]));
                        }
                    }
                    break;

                case NotifyCollectionChangedAction.Move:
                    validate_old_index();
                    validate_new_index();
                    if (e.OldStartingIndex < 0)
                    {
                        throw new InvalidOperationException($"Cannot process '{e.Action}' event when e.OldStartingIndex is less than 0.");
                    }
                    for (int i = 0; i < e.OldItems.Count; i++)
                    {
                        target.RemoveAt(e.OldStartingIndex);
                    }
                    if (e.NewStartingIndex < 0)
                    {
                        throw new InvalidOperationException($"Cannot process '{e.Action}' event when e.NewStartingIndex is less than 0.");
                    }
                    for (int i = 0; i < e.NewItems.Count; i++)
                    {
                    }
                    break;

                case NotifyCollectionChangedAction.Reset:
                    if (on_removed != null)
                    {
                        for (int i = 0; i < target.Count; i++)
                        {
                            on_removed(target[i]);
                        }
                    }
                    target.Clear();
                    for (int i = 0; i < source.Count; i++)
                    {
                        target.Add(create(source[i]));
                    }
                    break;
                }
                void validate_old_index() => validate_index(e.OldStartingIndex, nameof(NotifyCollectionChangedEventArgs.OldStartingIndex));
                void validate_new_index() => validate_index(e.NewStartingIndex, nameof(NotifyCollectionChangedEventArgs.NewStartingIndex));

                void validate_index(int i, string name)
                {
                    if (i < 0)
                    {
                        throw new InvalidOperationException(fmt_error($"{name} is less than 0."));
                    }
                }

                string fmt_error(string msg) => $"Cannot process '{e.Action}' event when {msg}";
            }
        }