ソースを参照

Site Documents interim release

Frank van den Bos 2 年 前
コミット
f78ca2dbfb

+ 2 - 3
InABox.Client.RPC/Transports/Socket/RPCClientSocketTransport.cs

@@ -15,7 +15,7 @@ namespace InABox.Rpc
         private WebSocket? _socket;
         
         private string[] _urls;
-        
+
         public RpcClientSocketTransport(string[] urls)
         {
             _urls = urls;
@@ -46,8 +46,7 @@ namespace InABox.Rpc
         {
             DoException(e.Exception);
         }
-
-
+        
         private WebSocket? CreateSocket(string url, bool secure)
         {
             var address = $"{(secure ? "wss" : "ws")}://{url}";

+ 1 - 0
InABox.Mobile/Shared/InABox.Mobile.Shared.projitems

@@ -18,6 +18,7 @@
     <Compile Include="$(MSBuildThisFileDirectory)MobileLogging.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)MobileUtils.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)HideableToolbarItem.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)ObservableRangeCollection.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)WrapLayout.cs" />
   </ItemGroup>
 </Project>

+ 668 - 0
InABox.Mobile/Shared/ObservableRangeCollection.cs

@@ -0,0 +1,668 @@
+using System;
+using System.Collections;
+using System.Collections.ObjectModel;
+
+namespace InABox.Mobile
+{
+  // Licensed to the .NET Foundation under one or more agreements.
+  // The .NET Foundation licenses this file to you under the MIT license.
+  // See the LICENSE file in the project root for more information.
+
+  using System.Collections.Generic;
+  using System.Collections.Specialized;
+  using System.ComponentModel;
+  using System.Diagnostics;
+  using System.Linq;
+
+  /// <summary>
+  /// Implementation of a dynamic data collection based on generic Collection&lt;T&gt;,
+  /// implementing INotifyCollectionChanged to notify listeners
+  /// when items get added, removed or the whole list is refreshed.
+  /// </summary>
+  public class CoreObservableCollection<T> : ObservableCollection<T>
+  {
+    //------------------------------------------------------
+    //
+    //  Private Fields
+    //
+    //------------------------------------------------------
+
+    #region Private Fields    
+    [NonSerialized]
+    private DeferredEventsCollection? _deferredEvents;
+    #endregion Private Fields
+
+
+    //------------------------------------------------------
+    //
+    //  Constructors
+    //
+    //------------------------------------------------------
+
+    #region Constructors
+    /// <summary>
+    /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
+    /// </summary>
+    public CoreObservableCollection() { }
+
+    /// <summary>
+    /// Initializes a new instance of the ObservableCollection class that contains
+    /// elements copied from the specified collection and has sufficient capacity
+    /// to accommodate the number of elements copied.
+    /// </summary>
+    /// <param name="collection">The collection whose elements are copied to the new list.</param>
+    /// <remarks>
+    /// The elements are copied onto the ObservableCollection in the
+    /// same order they are read by the enumerator of the collection.
+    /// </remarks>
+    /// <exception cref="ArgumentNullException"> collection is a null reference </exception>
+    public CoreObservableCollection(IEnumerable<T> collection) : base(collection) { }
+
+    /// <summary>
+    /// Initializes a new instance of the ObservableCollection class
+    /// that contains elements copied from the specified list
+    /// </summary>
+    /// <param name="list">The list whose elements are copied to the new list.</param>
+    /// <remarks>
+    /// The elements are copied onto the ObservableCollection in the
+    /// same order they are read by the enumerator of the list.
+    /// </remarks>
+    /// <exception cref="ArgumentNullException"> list is a null reference </exception>
+    public CoreObservableCollection(List<T> list) : base(list) { }
+
+    #endregion Constructors
+
+    //------------------------------------------------------
+    //
+    //  Public Properties
+    //
+    //------------------------------------------------------
+
+    #region Public Properties
+    EqualityComparer<T>? _Comparer;
+    public EqualityComparer<T> Comparer
+    {
+      get => _Comparer ??= EqualityComparer<T>.Default;
+      private set => _Comparer = value;
+    }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether this collection acts as a <see cref="HashSet{T}"/>,
+    /// disallowing duplicate items, based on <see cref="Comparer"/>.
+    /// This might indeed consume background performance, but in the other hand,
+    /// it will pay off in UI performance as less required UI updates are required.
+    /// </summary>
+    public bool AllowDuplicates { get; set; } = true;
+
+    #endregion Public Properties
+
+    //------------------------------------------------------
+    //
+    //  Public Methods
+    //
+    //------------------------------------------------------
+
+    #region Public Methods
+
+    /// <summary>
+    /// Adds the elements of the specified collection to the end of the <see cref="ObservableCollection{T}"/>.
+    /// </summary>
+    /// <param name="collection">
+    /// The collection whose elements should be added to the end of the <see cref="ObservableCollection{T}"/>.
+    /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
+    /// </param>
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    public void AddRange(IEnumerable<T> collection)
+    {
+      InsertRange(Count, collection);
+    }
+
+    /// <summary>
+    /// Inserts the elements of a collection into the <see cref="ObservableCollection{T}"/> at the specified index.
+    /// </summary>
+    /// <param name="index">The zero-based index at which the new elements should be inserted.</param>
+    /// <param name="collection">The collection whose elements should be inserted into the List<T>.
+    /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.</param>                
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not in the collection range.</exception>
+    public void InsertRange(int index, IEnumerable<T> collection)
+    {
+      if (collection == null)
+        throw new ArgumentNullException(nameof(collection));
+      if (index < 0)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (index > Count)
+        throw new ArgumentOutOfRangeException(nameof(index));
+
+      if (!AllowDuplicates)
+        collection =
+          collection
+          .Distinct(Comparer)
+          .Where(item => !Items.Contains(item, Comparer))
+          .ToList();
+
+      if (collection is ICollection<T> countable)
+      {
+        if (countable.Count == 0)
+          return;
+      }
+      else if (!collection.Any())
+        return;
+
+      CheckReentrancy();
+
+      //expand the following couple of lines when adding more constructors.
+      var target = (List<T>)Items;
+      target.InsertRange(index, collection);
+
+      OnEssentialPropertiesChanged();
+
+      if (!(collection is IList list))
+        list = new List<T>(collection);
+
+      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
+    }
+
+
+    /// <summary> 
+    /// Removes the first occurence of each item in the specified collection from the <see cref="ObservableCollection{T}"/>.
+    /// </summary>
+    /// <param name="collection">The items to remove.</param>        
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    public void RemoveRange(IEnumerable<T> collection)
+    {
+      if (collection == null)
+        throw new ArgumentNullException(nameof(collection));
+
+      if (Count == 0)
+        return;
+      else if (collection is ICollection<T> countable)
+      {
+        if (countable.Count == 0)
+          return;
+        else if (countable.Count == 1)
+          using (IEnumerator<T> enumerator = countable.GetEnumerator())
+          {
+            enumerator.MoveNext();
+            Remove(enumerator.Current);
+            return;
+          }
+      }
+      else if (!collection.Any())
+        return;
+
+      CheckReentrancy();
+
+      var clusters = new Dictionary<int, List<T>>();
+      var lastIndex = -1;
+      List<T>? lastCluster = null;
+      foreach (T item in collection)
+      {
+        var index = IndexOf(item);
+        if (index < 0)
+          continue;
+
+        Items.RemoveAt(index);
+
+        if (lastIndex == index && lastCluster != null)
+          lastCluster.Add(item);
+        else
+          clusters[lastIndex = index] = lastCluster = new List<T> { item };
+      }
+
+      OnEssentialPropertiesChanged();
+
+      if (Count == 0)
+        OnCollectionReset();
+      else
+        foreach (KeyValuePair<int, List<T>> cluster in clusters)
+          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster.Value, cluster.Key));
+
+    }
+
+    /// <summary>
+    /// Iterates over the collection and removes all items that satisfy the specified match.
+    /// </summary>
+    /// <remarks>The complexity is O(n).</remarks>
+    /// <param name="match"></param>
+    /// <returns>Returns the number of elements that where </returns>
+    /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
+    public int RemoveAll(Predicate<T> match)
+    {
+      return RemoveAll(0, Count, match);
+    }
+
+    /// <summary>
+    /// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
+    /// </summary>
+    /// <remarks>The complexity is O(n).</remarks>
+    /// <param name="index">The index of where to start performing the search.</param>
+    /// <param name="count">The number of items to iterate on.</param>
+    /// <param name="match"></param>
+    /// <returns>Returns the number of elements that where </returns>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
+    /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
+    public int RemoveAll(int index, int count, Predicate<T> match)
+    {
+      if (index < 0)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (count < 0)
+        throw new ArgumentOutOfRangeException(nameof(count));
+      if (index + count > Count)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (match == null)
+        throw new ArgumentNullException(nameof(match));
+
+      if (Count == 0)
+        return 0;
+
+      List<T>? cluster = null;
+      var clusterIndex = -1;
+      var removedCount = 0;
+
+      using (BlockReentrancy())
+      using (DeferEvents())
+      {
+        for (var i = 0; i < count; i++, index++)
+        {
+          T item = Items[index];
+          if (match(item))
+          {
+            Items.RemoveAt(index);
+            removedCount++;
+
+            if (clusterIndex == index)
+            {
+              Debug.Assert(cluster != null);
+              cluster!.Add(item);
+            }
+            else
+            {
+              cluster = new List<T> { item };
+              clusterIndex = index;
+            }
+
+            index--;
+          }
+          else if (clusterIndex > -1)
+          {
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
+            clusterIndex = -1;
+            cluster = null;
+          }
+        }
+
+        if (clusterIndex > -1)
+          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
+      }
+
+      if (removedCount > 0)
+        OnEssentialPropertiesChanged();
+
+      return removedCount;
+    }
+
+    /// <summary>
+    /// Removes a range of elements from the <see cref="ObservableCollection{T}"/>>.
+    /// </summary>
+    /// <param name="index">The zero-based starting index of the range of elements to remove.</param>
+    /// <param name="count">The number of elements to remove.</param>
+    /// <exception cref="ArgumentOutOfRangeException">The specified range is exceeding the collection.</exception>
+    public void RemoveRange(int index, int count)
+    {
+      if (index < 0)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (count < 0)
+        throw new ArgumentOutOfRangeException(nameof(count));
+      if (index + count > Count)
+        throw new ArgumentOutOfRangeException(nameof(index));
+
+      if (count == 0)
+        return;
+
+      if (count == 1)
+      {
+        RemoveItem(index);
+        return;
+      }
+
+      //Items will always be List<T>, see constructors
+      var items = (List<T>)Items;
+      List<T> removedItems = items.GetRange(index, count);
+
+      CheckReentrancy();
+
+      items.RemoveRange(index, count);
+
+      OnEssentialPropertiesChanged();
+
+      if (Count == 0)
+        OnCollectionReset();
+      else
+        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index));
+    }
+
+    /// <summary> 
+    /// Clears the current collection and replaces it with the specified collection,
+    /// using <see cref="Comparer"/>.
+    /// </summary>             
+    /// <param name="collection">The items to fill the collection with, after clearing it.</param>
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    public void ReplaceRange(IEnumerable<T> collection)
+    {
+      ReplaceRange(0, Count, collection);
+    }
+
+    /// <summary>
+    /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
+    /// </summary>
+    /// <param name="index">The index of where to start the replacement.</param>
+    /// <param name="count">The number of items to be replaced.</param>
+    /// <param name="collection">The collection to insert in that location.</param>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null.</exception>
+    public void ReplaceRange(int index, int count, IEnumerable<T> collection)
+    {
+      if (index < 0)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (count < 0)
+        throw new ArgumentOutOfRangeException(nameof(count));
+      if (index + count > Count)
+        throw new ArgumentOutOfRangeException(nameof(index));
+
+      if (collection == null)
+        throw new ArgumentNullException(nameof(collection));
+
+      if (!AllowDuplicates)
+        collection =
+          collection
+          .Distinct(Comparer)
+          .ToList();
+
+      if (collection is ICollection<T> countable)
+      {
+        if (countable.Count == 0)
+        {
+          RemoveRange(index, count);
+          return;
+        }
+      }
+      else if (!collection.Any())
+      {
+        RemoveRange(index, count);
+        return;
+      }
+
+      if (index + count == 0)
+      {
+        InsertRange(0, collection);
+        return;
+      }
+
+      if (!(collection is IList<T> list))
+        list = new List<T>(collection);
+
+      using (BlockReentrancy())
+      using (DeferEvents())
+      {
+        var rangeCount = index + count;
+        var addedCount = list.Count;
+
+        var changesMade = false;
+        List<T>?
+          newCluster = null,
+          oldCluster = null;
+
+
+        int i = index;
+        for (; i < rangeCount && i - index < addedCount; i++)
+        {
+          //parallel position
+          T old = this[i], @new = list[i - index];
+          if (Comparer.Equals(old, @new))
+          {
+            OnRangeReplaced(i, newCluster!, oldCluster!);
+            continue;
+          }
+          else
+          {
+            Items[i] = @new;
+
+            if (newCluster == null)
+            {
+              Debug.Assert(oldCluster == null);
+              newCluster = new List<T> { @new };
+              oldCluster = new List<T> { old };
+            }
+            else
+            {
+              newCluster.Add(@new);
+              oldCluster!.Add(old);
+            }
+
+            changesMade = true;
+          }
+        }
+
+        OnRangeReplaced(i, newCluster!, oldCluster!);
+
+        //exceeding position
+        if (count != addedCount)
+        {
+          var items = (List<T>)Items;
+          if (count > addedCount)
+          {
+            var removedCount = rangeCount - addedCount;
+            T[] removed = new T[removedCount];
+            items.CopyTo(i, removed, 0, removed.Length);
+            items.RemoveRange(i, removedCount);
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed, i));
+          }
+          else
+          {
+            var k = i - index;
+            T[] added = new T[addedCount - k];
+            for (int j = k; j < addedCount; j++)
+            {
+              T @new = list[j];
+              added[j - k] = @new;
+            }
+            items.InsertRange(i, added);
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, added, i));
+          }
+
+          OnEssentialPropertiesChanged();
+        }
+        else if (changesMade)
+        {
+          OnIndexerPropertyChanged();
+        }
+      }
+    }
+
+    #endregion Public Methods
+
+
+    //------------------------------------------------------
+    //
+    //  Protected Methods
+    //
+    //------------------------------------------------------
+
+    #region Protected Methods
+
+    /// <summary>
+    /// Called by base class Collection&lt;T&gt; when the list is being cleared;
+    /// raises a CollectionChanged event to any listeners.
+    /// </summary>
+    protected override void ClearItems()
+    {
+      if (Count == 0)
+        return;
+
+      CheckReentrancy();
+      base.ClearItems();
+      OnEssentialPropertiesChanged();
+      OnCollectionReset();
+    }
+
+    /// <inheritdoc/>
+    protected override void InsertItem(int index, T item)
+    {
+      if (!AllowDuplicates && Items.Contains(item))
+        return;
+
+      base.InsertItem(index, item);
+    }
+
+    /// <inheritdoc/>
+    protected override void SetItem(int index, T item)
+    {
+      if (AllowDuplicates)
+      {
+        if (Comparer.Equals(this[index], item))
+          return;
+      }
+      else
+        if (Items.Contains(item, Comparer))
+        return;
+
+      CheckReentrancy();
+      T oldItem = this[index];
+      base.SetItem(index, item);
+
+      OnIndexerPropertyChanged();
+      OnCollectionChanged(NotifyCollectionChangedAction.Replace, oldItem!, item!, index);
+    }
+
+    /// <summary>
+    /// Raise CollectionChanged event to any listeners.
+    /// Properties/methods modifying this ObservableCollection will raise
+    /// a collection changed event through this virtual method.
+    /// </summary>
+    /// <remarks>
+    /// When overriding this method, either call its base implementation
+    /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
+    /// </remarks>
+    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+    {
+      if (_deferredEvents != null)
+      {
+        _deferredEvents.Add(e);
+        return;
+      }
+      base.OnCollectionChanged(e);
+    }
+
+    protected virtual IDisposable DeferEvents() => new DeferredEventsCollection(this);
+
+    #endregion Protected Methods
+
+
+    //------------------------------------------------------
+    //
+    //  Private Methods
+    //
+    //------------------------------------------------------
+
+    #region Private Methods
+
+    /// <summary>
+    /// Helper to raise Count property and the Indexer property.
+    /// </summary>
+    void OnEssentialPropertiesChanged()
+    {
+      OnPropertyChanged(EventArgsCache.CountPropertyChanged);
+      OnIndexerPropertyChanged();
+    }
+
+    /// <summary>
+    /// /// Helper to raise a PropertyChanged event for the Indexer property
+    /// /// </summary>
+    void OnIndexerPropertyChanged() =>
+     OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
+
+    /// <summary>
+    /// Helper to raise CollectionChanged event to any listeners
+    /// </summary>
+    void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
+     OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
+
+    /// <summary>
+    /// Helper to raise CollectionChanged event with action == Reset to any listeners
+    /// </summary>
+    void OnCollectionReset() =>
+     OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
+
+    /// <summary>
+    /// Helper to raise event for clustered action and clear cluster.
+    /// </summary>
+    /// <param name="followingItemIndex">The index of the item following the replacement block.</param>
+    /// <param name="newCluster"></param>
+    /// <param name="oldCluster"></param>
+    //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable<T> collection, IEqualityComparer<T> comparer),
+    //move when supported language version updated.
+    void OnRangeReplaced(int followingItemIndex, ICollection<T> newCluster, ICollection<T> oldCluster)
+    {
+      if (oldCluster == null || oldCluster.Count == 0)
+      {
+        Debug.Assert(newCluster == null || newCluster.Count == 0);
+        return;
+      }
+
+      OnCollectionChanged(
+        new NotifyCollectionChangedEventArgs(
+          NotifyCollectionChangedAction.Replace,
+          new List<T>(newCluster),
+          new List<T>(oldCluster),
+          followingItemIndex - oldCluster.Count));
+
+      oldCluster.Clear();
+      newCluster.Clear();
+    }
+
+    #endregion Private Methods
+
+    //------------------------------------------------------
+    //
+    //  Private Types
+    //
+    //------------------------------------------------------
+
+    #region Private Types
+    sealed class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
+    {
+      readonly CoreObservableCollection<T> _collection;
+      public DeferredEventsCollection(CoreObservableCollection<T> collection)
+      {
+        Debug.Assert(collection != null);
+        Debug.Assert(collection._deferredEvents == null);
+        _collection = collection;
+        _collection._deferredEvents = this;
+      }
+
+      public void Dispose()
+      {
+        _collection._deferredEvents = null;
+        foreach (var args in this)
+          _collection.OnCollectionChanged(args);
+      }
+    }
+
+    #endregion Private Types
+
+  }
+
+  /// <remarks>
+  /// To be kept outside <see cref="ObservableCollection{T}"/>, since otherwise, a new instance will be created for each generic type used.
+  /// </remarks>
+  internal static class EventArgsCache
+  {
+    internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs("Count");
+    internal static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs("Item[]");
+    internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+  }
+}