diff --git a/.editorconfig b/.editorconfig index 434fb0c8e..0454e2555 100644 --- a/.editorconfig +++ b/.editorconfig @@ -28,6 +28,7 @@ dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_compound_assignment = true:suggestion dotnet_style_prefer_simplified_interpolation = true:suggestion dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_prefer_collection_expression = true:suggestion [project.json] indent_size = 2 @@ -492,7 +493,7 @@ dotnet_diagnostic.SA1202.severity = silent dotnet_diagnostic.SA1203.severity = error -dotnet_diagnostic.SA1204.severity = error +dotnet_diagnostic.SA1204.severity = none dotnet_diagnostic.SA1205.severity = error @@ -548,7 +549,7 @@ dotnet_diagnostic.SA1400.severity = error dotnet_diagnostic.SA1401.severity = error -dotnet_diagnostic.SA1402.severity = error +dotnet_diagnostic.SA1402.severity = none dotnet_diagnostic.SA1403.severity = error @@ -697,6 +698,7 @@ dotnet_diagnostic.SX1309S.severity=silent csharp_style_namespace_declarations = block_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion # C++ Files [*.{cpp,h,in}] diff --git a/src/DynamicData.Tests/Binding/IObservableListBindListFixture.cs b/src/DynamicData.Tests/Binding/IObservableListBindListFixture.cs index 2eccac232..046b2ce36 100644 --- a/src/DynamicData.Tests/Binding/IObservableListBindListFixture.cs +++ b/src/DynamicData.Tests/Binding/IObservableListBindListFixture.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Linq; - +using System.Reactive.Linq; using DynamicData.Binding; using DynamicData.Tests.Domain; @@ -30,6 +32,98 @@ public IObservableListBindListFixture() _observableListNotifications = _list.Connect().AsAggregator(); } + [Fact] + public void ResetThresholdsForBinding_ObservableCollection() + { + var people = _generator.Take(100).ToArray(); + + // check whether reset is fired with different params + var test1 = Test(); + var test2 = Test(new BindingOptions(95)); + var test3 = Test(new BindingOptions(105, ResetOnFirstTimeLoad: false)); + var test4 = Test(BindingOptions.NeverFireReset()); + + + test1.action.Should().Be(NotifyCollectionChangedAction.Reset); + test2.action.Should().Be(NotifyCollectionChangedAction.Reset); + test3.action.Should().Be(NotifyCollectionChangedAction.Add); + test4.action.Should().Be(NotifyCollectionChangedAction.Add); + + return; + + (NotifyCollectionChangedAction action, ObservableCollectionExtended list) Test(BindingOptions? options = null) + { + _source.Clear(); + + NotifyCollectionChangedAction? result = null; + + var list = new ObservableCollectionExtended(); + using var listEvents = list.ObserveCollectionChanges().Take(1) + .Select(e => e.EventArgs.Action) + .Subscribe(events => + { + result = events; + }); + + + var binder = options == null + ? _source.Connect().Bind(list).Subscribe() + : _source.Connect().Bind(list, options.Value).Subscribe(); + + _source.AddRange(people); + binder.Dispose(); + + return (result!.Value, list); + } + } + + [Fact] + public void ResetThresholdsForBinding_ReadonlyObservableCollection() + { + var people = _generator.Take(100).ToArray(); + + + // check whether reset is fired with different params + var test1 = Test(); + var test2 = Test(new BindingOptions(95)); + var test3 = Test(new BindingOptions(105, ResetOnFirstTimeLoad: false)); + var test4 = Test(BindingOptions.NeverFireReset()); + + + test1.action.Should().Be(NotifyCollectionChangedAction.Reset); + test2.action.Should().Be(NotifyCollectionChangedAction.Reset); + test3.action.Should().Be(NotifyCollectionChangedAction.Add); + test4.action.Should().Be(NotifyCollectionChangedAction.Add); + + return; + + (NotifyCollectionChangedAction action, ReadOnlyObservableCollection list) Test(BindingOptions? options = null) + { + _source.Clear(); + + NotifyCollectionChangedAction? result = null; + ReadOnlyObservableCollection list; + //var list = new ObservableCollectionExtended(); + + var binder = options == null + ? _source.Connect().Bind(out list).Subscribe() + : _source.Connect().Bind(out list, options.Value).Subscribe(); + + using var listEvents = list.ObserveCollectionChanges().Take(1) + .Select(e => e.EventArgs.Action) + .Subscribe(events => + { + result = events; + }); + + _source.AddRange(people); + binder.Dispose(); + return (result!.Value, list); + } + } + + + [Fact] public void AddRange() { diff --git a/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheFixture.cs b/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheFixture.cs index 70be9699d..6d68b12c0 100644 --- a/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheFixture.cs +++ b/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Reactive.Linq; @@ -28,6 +29,99 @@ public ObservableCollectionBindCacheFixture() _binder = _source.Connect().Bind(_collection).Subscribe(); } + [Fact] + public void ResetThresholdsForBinding_ObservableCollection() + { + var people = _generator.Take(100).ToArray(); + + + + // check whether reset is fired with different params + var test1 = Test(); + var test2 = Test(new BindingOptions(95)); + var test3 = Test(new BindingOptions(105, ResetOnFirstTimeLoad: false)); + var test4 = Test(BindingOptions.NeverFireReset()); + + + test1.action.Should().Be(NotifyCollectionChangedAction.Reset); + test2.action.Should().Be(NotifyCollectionChangedAction.Reset); + test3.action.Should().Be(NotifyCollectionChangedAction.Add); + test4.action.Should().Be(NotifyCollectionChangedAction.Add); + + return; + + (NotifyCollectionChangedAction action, ObservableCollectionExtended list) Test(BindingOptions? options = null) + { + _source.Clear(); + + NotifyCollectionChangedAction? result = null; + + var list = new ObservableCollectionExtended(); + using var listEvents = list.ObserveCollectionChanges().Take(1) + .Select(e => e.EventArgs.Action) + .Subscribe(events => + { + result = events; + }); + + + var binder = options == null + ? _source.Connect().Bind(list).Subscribe() + : _source.Connect().Bind(list, options.Value).Subscribe(); + + _source.AddOrUpdate(people); + binder.Dispose(); + + return (result!.Value, list); + } + } + + [Fact] + public void ResetThresholdsForBinding_ReadonlyObservableCollection() + { + var people = _generator.Take(100).ToArray(); + + + // check whether reset is fired with different params + var test1 = Test(); + var test2 = Test(new BindingOptions(95)); + var test3 = Test(new BindingOptions(105, ResetOnFirstTimeLoad: false)); + var test4 = Test(BindingOptions.NeverFireReset()); + + + test1.action.Should().Be(NotifyCollectionChangedAction.Reset); + test2.action.Should().Be(NotifyCollectionChangedAction.Reset); + test3.action.Should().Be(NotifyCollectionChangedAction.Add); + test4.action.Should().Be(NotifyCollectionChangedAction.Add); + + return; + + (NotifyCollectionChangedAction action, ReadOnlyObservableCollection list) Test(BindingOptions? options = null) + { + _source.Clear(); + + NotifyCollectionChangedAction? result = null; + ReadOnlyObservableCollection list; + //var list = new ObservableCollectionExtended(); + + var binder = options == null + ? _source.Connect().Bind(out list).Subscribe() + : _source.Connect().Bind(out list, options.Value).Subscribe(); + + using var listEvents = list.ObserveCollectionChanges().Take(1) + .Select(e => e.EventArgs.Action) + .Subscribe(events => + { + result = events; + }); + + _source.AddOrUpdate(people); + binder.Dispose(); + return (result!.Value, list); + } + } + + [Fact] public void AddToSourceAddsToDestination() { diff --git a/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheSortedFixture.cs b/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheSortedFixture.cs index 4566d7a4a..c5df8ce24 100644 --- a/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheSortedFixture.cs +++ b/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheSortedFixture.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Reactive.Linq; @@ -8,7 +9,6 @@ using DynamicData.Tests.Domain; using FluentAssertions; - using Xunit; namespace DynamicData.Tests.Binding; @@ -17,11 +17,11 @@ public class ObservableCollectionBindCacheSortedFixture : IDisposable { private readonly IDisposable _binder; - private readonly ObservableCollectionExtended _collection = new ObservableCollectionExtended(); + private readonly ObservableCollectionExtended _collection; private readonly IComparer _comparer = SortExpressionComparer.Ascending(p => p.Name); - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + private readonly RandomPersonGenerator _generator = new(); private readonly ISourceCache _source; @@ -32,6 +32,99 @@ public ObservableCollectionBindCacheSortedFixture() _binder = _source.Connect().Sort(_comparer, resetThreshold: 25).Bind(_collection).Subscribe(); } + + [Fact] + public void ResetThresholdsForBinding_ObservableCollection() + { + var people = _generator.Take(100).ToArray(); + + + + // check whether reset is fired with different params + var test1 = Test(); + var test2 = Test(new BindingOptions(95)); + var test3 = Test(new BindingOptions(105, ResetOnFirstTimeLoad: false)); + var test4 = Test(BindingOptions.NeverFireReset()); + + + test1.action.Should().Be(NotifyCollectionChangedAction.Reset); + test2.action.Should().Be(NotifyCollectionChangedAction.Reset); + test3.action.Should().Be(NotifyCollectionChangedAction.Add); + test4.action.Should().Be(NotifyCollectionChangedAction.Add); + + return; + + (NotifyCollectionChangedAction action, ObservableCollectionExtended list) Test(BindingOptions? options = null) + { + _source.Clear(); + + NotifyCollectionChangedAction? result = null; + + var list = new ObservableCollectionExtended(); + using var listEvents = list.ObserveCollectionChanges().Take(1) + .Select(e => e.EventArgs.Action) + .Subscribe(events => + { + result = events; + }); + + + var binder = options == null + ? _source.Connect().Sort(_comparer).Bind(list).Subscribe() + : _source.Connect().Sort(_comparer).Bind(list, options.Value).Subscribe(); + + _source.AddOrUpdate(people); + binder.Dispose(); + + return (result!.Value, list); + } + } + + [Fact] + public void ResetThresholdsForBinding_ReadonlyObservableCollection() + { + var people = _generator.Take(100).ToArray(); + + + // check whether reset is fired with different params + var test1 = Test(); + var test2 = Test(new BindingOptions(95)); + var test3 = Test(new BindingOptions(105, ResetOnFirstTimeLoad: false)); + var test4 = Test(BindingOptions.NeverFireReset()); + + + test1.action.Should().Be(NotifyCollectionChangedAction.Reset); + test2.action.Should().Be(NotifyCollectionChangedAction.Reset); + test3.action.Should().Be(NotifyCollectionChangedAction.Add); + test4.action.Should().Be(NotifyCollectionChangedAction.Add); + + return; + + (NotifyCollectionChangedAction action, ReadOnlyObservableCollection list) Test(BindingOptions? options = null) + { + _source.Clear(); + + NotifyCollectionChangedAction? result = null; + ReadOnlyObservableCollection list; + //var list = new ObservableCollectionExtended(); + + var binder = options == null + ? _source.Connect().Sort(_comparer).Bind(out list).Subscribe() + : _source.Connect().Sort(_comparer).Bind(out list, options.Value).Subscribe(); + + using var listEvents = list.ObserveCollectionChanges().Take(1) + .Select(e => e.EventArgs.Action) + .Subscribe(events => + { + result = events; + }); + + _source.AddOrUpdate(people); + binder.Dispose(); + return (result!.Value, list); + } + } + [Fact] public void AddToSourceAddsToDestination() { diff --git a/src/DynamicData/Binding/BindingListAdaptor.cs b/src/DynamicData/Binding/BindingListAdaptor.cs index d11944e7b..8198e37dd 100644 --- a/src/DynamicData/Binding/BindingListAdaptor.cs +++ b/src/DynamicData/Binding/BindingListAdaptor.cs @@ -28,7 +28,7 @@ public class BindingListAdaptor : IChangeSetAdaptor /// /// The list of items to add to the adapter. /// The threshold before a reset is issued. - public BindingListAdaptor(BindingList list, int refreshThreshold = 25) + public BindingListAdaptor(BindingList list, int refreshThreshold = BindingOptions.DefaultResetThreshold) { _list = list ?? throw new ArgumentNullException(nameof(list)); _refreshThreshold = refreshThreshold; @@ -80,7 +80,7 @@ public class BindingListAdaptor : IChangeSetAdaptor /// The list of items to adapt. /// The threshold before the refresh is triggered. - public BindingListAdaptor(BindingList list, int refreshThreshold = 25) + public BindingListAdaptor(BindingList list, int refreshThreshold = BindingOptions.DefaultResetThreshold) { _list = list ?? throw new ArgumentNullException(nameof(list)); _refreshThreshold = refreshThreshold; diff --git a/src/DynamicData/Binding/BindingOptions.cs b/src/DynamicData/Binding/BindingOptions.cs new file mode 100644 index 000000000..dc823126f --- /dev/null +++ b/src/DynamicData/Binding/BindingOptions.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. +// Roland Pheasant licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace DynamicData.Binding; + +/// +/// System wide default values for binding operators. +/// +/// The reset threshold ie the number of changes before a reset is fired. +/// Should a reset be fired for a first time load.This option is due to historic reasons where a reset would be fired for the first time load regardless of the number of changes. +/// When possible, should replace be used instead of remove and add. +public record struct BindingOptions(int ResetThreshold, bool UseReplaceForUpdates = BindingOptions.DefaultUseReplaceForUpdates, bool ResetOnFirstTimeLoad = BindingOptions.DefaultResetOnFirstTimeLoad) +{ + /// + /// The system wide factory settings default ResetThreshold. + /// + public const int DefaultResetThreshold = 25; + + /// + /// The system wide factory settings default UseReplaceForUpdates value. + /// + public const bool DefaultUseReplaceForUpdates = true; + + /// + /// The system wide factory settings default ResetOnFirstTimeLoad value. + /// + public const bool DefaultResetOnFirstTimeLoad = true; + + /// + /// Creates binding options to never fire a reset event. + /// + /// When possible, should replace be used instead of remove and add. + /// The binding options. + public static BindingOptions NeverFireReset(bool useReplaceForUpdates = DefaultResetOnFirstTimeLoad) + { + return new BindingOptions(int.MaxValue, useReplaceForUpdates, false); + } +} + +/// +/// System wide default values for binding operators. +/// +public static class DynamicDataOptions +{ + /// + /// Gets or sets the default values for all binding operations. + /// + public static BindingOptions Binding { get; set; } = new(BindingOptions.DefaultResetThreshold); +} diff --git a/src/DynamicData/Binding/ObservableCollectionAdaptor.cs b/src/DynamicData/Binding/ObservableCollectionAdaptor.cs index d83c7769b..c5148639b 100644 --- a/src/DynamicData/Binding/ObservableCollectionAdaptor.cs +++ b/src/DynamicData/Binding/ObservableCollectionAdaptor.cs @@ -16,14 +16,40 @@ namespace DynamicData.Binding; /// Initializes a new instance of the class. /// /// The collection. -/// The refresh threshold. +/// The number of changes before a Reset event is used. +/// Use replace instead of remove / add for updates. +/// Should a reset be fired for a first time load.This option is due to historic reasons where a reset would be fired for the first time load regardless of the number of changes. /// collection. -public class ObservableCollectionAdaptor(IObservableCollection collection, int refreshThreshold = 25) : IChangeSetAdaptor +public class ObservableCollectionAdaptor(IObservableCollection collection, int refreshThreshold, +#pragma warning disable CS9113 // Parameter is unread. + bool allowReplace = true, +#pragma warning restore CS9113 // Parameter is unread. + bool resetOnFirstTimeLoad = true) : IChangeSetAdaptor + where T : notnull { private readonly IObservableCollection _collection = collection ?? throw new ArgumentNullException(nameof(collection)); private bool _loaded; + /// + /// Initializes a new instance of the class. + /// + /// The collection. + public ObservableCollectionAdaptor(IObservableCollection collection) + : this(collection, DynamicDataOptions.Binding) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding options. + /// The collection. + public ObservableCollectionAdaptor(IObservableCollection collection, BindingOptions options) + : this(collection, options.ResetThreshold, options.UseReplaceForUpdates, options.ResetOnFirstTimeLoad) + { + } + /// /// Maintains the specified collection from the changes. /// @@ -35,7 +61,7 @@ public void Adapt(IChangeSet changes) throw new ArgumentNullException(nameof(changes)); } - if (changes.TotalChanges - changes.Refreshes > refreshThreshold || !_loaded) + if (changes.TotalChanges - changes.Refreshes > refreshThreshold || (!_loaded && resetOnFirstTimeLoad)) { using (_collection.SuspendNotifications()) { @@ -45,6 +71,7 @@ public void Adapt(IChangeSet changes) } else { + // TODO: pass in allowReplace to handle replace vs remove / add _collection.Clone(changes); } } @@ -61,14 +88,24 @@ public void Adapt(IChangeSet changes) /// /// The threshold before a reset notification is triggered. /// Use replace instead of remove / add for updates. +/// Should a reset be fired for a first time load.This option is due to historic reasons where a reset would be fired for the first time load regardless of the number of changes. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Same class name, only generic difference.")] -public class ObservableCollectionAdaptor(int refreshThreshold = 25, bool useReplaceForUpdates = false) : IObservableCollectionAdaptor +public class ObservableCollectionAdaptor(int refreshThreshold = 25, bool useReplaceForUpdates = true, bool resetOnFirstTimeLoad = true) : IObservableCollectionAdaptor where TObject : notnull where TKey : notnull { private readonly Cache _cache = new(); private bool _loaded; + /// + /// Initializes a new instance of the class. + /// + /// The binding options. + public ObservableCollectionAdaptor(BindingOptions options) + : this(options.ResetThreshold, options.UseReplaceForUpdates, options.ResetOnFirstTimeLoad) + { + } + /// /// Maintains the specified collection from the changes. /// @@ -88,7 +125,7 @@ public void Adapt(IChangeSet changes, IObservableCollection refreshThreshold || !_loaded) + if (changes.Count - changes.Refreshes > refreshThreshold || (!_loaded && resetOnFirstTimeLoad)) { _loaded = true; using (collection.SuspendNotifications()) diff --git a/src/DynamicData/Binding/SortedObservableCollectionAdaptor.cs b/src/DynamicData/Binding/SortedObservableCollectionAdaptor.cs index a104b5f9c..54f8bfe68 100644 --- a/src/DynamicData/Binding/SortedObservableCollectionAdaptor.cs +++ b/src/DynamicData/Binding/SortedObservableCollectionAdaptor.cs @@ -15,10 +15,28 @@ namespace DynamicData.Binding; /// /// The number of changes before a Reset event is used. /// Use replace instead of remove / add for updates. -public class SortedObservableCollectionAdaptor(int refreshThreshold = 25, bool useReplaceForUpdates = true) : ISortedObservableCollectionAdaptor +/// Should a reset be fired for a first time load.This option is due to historic reasons where a reset would be fired for the first time load regardless of the number of changes. +public class SortedObservableCollectionAdaptor(int refreshThreshold, bool useReplaceForUpdates = true, bool resetOnFirstTimeLoad = true) : ISortedObservableCollectionAdaptor where TObject : notnull where TKey : notnull { + /// + /// Initializes a new instance of the class. + /// + public SortedObservableCollectionAdaptor() + : this(DynamicDataOptions.Binding) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding options. + public SortedObservableCollectionAdaptor(BindingOptions options) + : this(options.ResetThreshold, options.UseReplaceForUpdates, options.ResetOnFirstTimeLoad) + { + } + /// /// Maintains the specified collection from the changes. /// @@ -38,16 +56,41 @@ public void Adapt(ISortedChangeSet changes, IObservableCollection switch (changes.SortedItems.SortReason) { - case SortReason.InitialLoad: case SortReason.ComparerChanged: case SortReason.Reset: - using (collection.SuspendNotifications()) + + // Multiply items count by 2 as we need to clear existing items + if (changes.SortedItems.Count * 2 > refreshThreshold) + { + using (collection.SuspendNotifications()) + { + collection.Load(changes.SortedItems.Select(kv => kv.Value)); + } + } + else { collection.Load(changes.SortedItems.Select(kv => kv.Value)); } break; + case SortReason.InitialLoad: + if (resetOnFirstTimeLoad && (changes.Count - changes.Refreshes > refreshThreshold)) + { + using (collection.SuspendNotifications()) + { + collection.Load(changes.SortedItems.Select(kv => kv.Value)); + } + } + else + { + using (collection.SuspendCount()) + { + DoUpdate(changes, collection); + } + } + + break; case SortReason.DataChanged: if (changes.Count - changes.Refreshes > refreshThreshold) { diff --git a/src/DynamicData/Cache/ObservableCacheEx.cs b/src/DynamicData/Cache/ObservableCacheEx.cs index 34f7a89b7..2d7bcda13 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.cs @@ -596,21 +596,42 @@ public static IObservable> BatchIf(this /// The number of changes before a reset notification is triggered. /// An observable which will emit change sets. /// source. - public static IObservable> Bind(this IObservable> source, IObservableCollection destination, int refreshThreshold = 25) + public static IObservable> Bind(this IObservable> source, IObservableCollection destination, int refreshThreshold = BindingOptions.DefaultResetThreshold) where TObject : notnull where TKey : notnull { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } + if (source is null) throw new ArgumentNullException(nameof(source)); + if (destination is null) throw new ArgumentNullException(nameof(destination)); - if (destination is null) - { - throw new ArgumentNullException(nameof(destination)); - } + // if user has not specified different defaults, use system wide defaults instead. + // This is a hack to retro fit system wide defaults which override the hard coded defaults above + var defaults = DynamicDataOptions.Binding; - return source.Bind(destination, new ObservableCollectionAdaptor(refreshThreshold)); + var options = refreshThreshold == BindingOptions.DefaultResetThreshold + ? defaults + : defaults with { ResetThreshold = refreshThreshold }; + + return source.Bind(destination, new ObservableCollectionAdaptor(options)); + } + + /// + /// Binds the results to the specified observable collection using the default update algorithm. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The destination. + /// The binding options. + /// An observable which will emit change sets. + /// source. + public static IObservable> Bind(this IObservable> source, IObservableCollection destination, BindingOptions options) + where TObject : notnull + where TKey : notnull + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (destination is null) throw new ArgumentNullException(nameof(destination)); + + return source.Bind(destination, new ObservableCollectionAdaptor(options)); } /// @@ -627,20 +648,9 @@ public static IObservable> Bind(this IO where TObject : notnull where TKey : notnull { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (destination is null) - { - throw new ArgumentNullException(nameof(destination)); - } - - if (updater is null) - { - throw new ArgumentNullException(nameof(updater)); - } + if (source is null) throw new ArgumentNullException(nameof(source)); + if (destination is null) throw new ArgumentNullException(nameof(destination)); + if (updater is null) throw new ArgumentNullException(nameof(updater)); return Observable.Create>( observer => @@ -656,15 +666,16 @@ public static IObservable> Bind(this IO } /// - /// Binds the results to the specified observable collection using the default update algorithm. + /// Binds the results to the specified readonly observable collection using the default update algorithm. /// /// The type of the object. /// The type of the key. /// The source. - /// The destination. + /// The resulting read only observable collection. + /// The binding options. /// An observable which will emit change sets. /// source. - public static IObservable> Bind(this IObservable> source, IObservableCollection destination) + public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, BindingOptions options) where TObject : notnull where TKey : notnull { @@ -673,26 +684,24 @@ public static IObservable> Bind(t throw new ArgumentNullException(nameof(source)); } - if (destination is null) - { - throw new ArgumentNullException(nameof(destination)); - } - - var updater = new SortedObservableCollectionAdaptor(); - return source.Bind(destination, updater); + var target = new ObservableCollectionExtended(); + readOnlyObservableCollection = new ReadOnlyObservableCollection(target); + return source.Bind(target, new ObservableCollectionAdaptor(options)); } /// - /// Binds the results to the specified binding collection using the specified update algorithm. + /// Binds the results to the specified readonly observable collection using the default update algorithm. /// /// The type of the object. /// The type of the key. /// The source. - /// The destination. - /// The updater. + /// The resulting read only observable collection. + /// The number of changes before a reset notification is triggered. + /// Use replace instead of remove / add for updates. NB: Some platforms to not support replace notifications for binding. + /// Specify an adaptor to change the algorithm to update the target collection. /// An observable which will emit change sets. /// source. - public static IObservable> Bind(this IObservable> source, IObservableCollection destination, ISortedObservableCollectionAdaptor updater) + public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, int resetThreshold = BindingOptions.DefaultResetThreshold, bool useReplaceForUpdates = BindingOptions.DefaultUseReplaceForUpdates, IObservableCollectionAdaptor? adaptor = null) where TObject : notnull where TKey : notnull { @@ -701,15 +710,81 @@ public static IObservable> Bind(t throw new ArgumentNullException(nameof(source)); } - if (destination is null) + if (adaptor is not null) { - throw new ArgumentNullException(nameof(destination)); + var target = new ObservableCollectionExtended(); + readOnlyObservableCollection = new ReadOnlyObservableCollection(target); + return source.Bind(target, adaptor); } - if (updater is null) - { - throw new ArgumentNullException(nameof(updater)); - } + // if user has not specified different defaults, use system wide defaults instead. + // This is a hack to retro fit system wide defaults which override the hard coded defaults above + var defaults = DynamicDataOptions.Binding; + + var options = resetThreshold == BindingOptions.DefaultResetThreshold && useReplaceForUpdates == BindingOptions.DefaultUseReplaceForUpdates + ? defaults + : defaults with { ResetThreshold = resetThreshold, UseReplaceForUpdates = useReplaceForUpdates }; + + return source.Bind(out readOnlyObservableCollection, options); + } + + /// + /// Binds the results to the specified observable collection using the default update algorithm. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The destination. + /// An observable which will emit change sets. + /// source. + public static IObservable> Bind(this IObservable> source, IObservableCollection destination) + where TObject : notnull + where TKey : notnull + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (destination is null) throw new ArgumentNullException(nameof(destination)); + + return source.Bind(destination, DynamicDataOptions.Binding); + } + + /// + /// Binds the results to the specified observable collection using the default update algorithm. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The destination. + /// The binding options. + /// An observable which will emit change sets. + /// source. + public static IObservable> Bind(this IObservable> source, IObservableCollection destination, BindingOptions options) + where TObject : notnull + where TKey : notnull + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (destination is null) throw new ArgumentNullException(nameof(destination)); + + var updater = new SortedObservableCollectionAdaptor(options); + return source.Bind(destination, updater); + } + + /// + /// Binds the results to the specified binding collection using the specified update algorithm. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The destination. + /// The updater. + /// An observable which will emit change sets. + /// source. + public static IObservable> Bind(this IObservable> source, IObservableCollection destination, ISortedObservableCollectionAdaptor updater) + where TObject : notnull + where TKey : notnull + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (destination is null) throw new ArgumentNullException(nameof(destination)); + if (updater is null) throw new ArgumentNullException(nameof(updater)); return Observable.Create>( observer => @@ -731,12 +806,10 @@ public static IObservable> Bind(t /// The type of the key. /// The source. /// The resulting read only observable collection. - /// The number of changes before a reset event is called on the observable collection. - /// Use replace instead of remove / add for updates. NB: Some platforms to not support replace notifications for binding. - /// Specify an adaptor to change the algorithm to update the target collection. + /// The binding options. /// An observable which will emit change sets. /// source. - public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, int resetThreshold = 25, bool useReplaceForUpdates = true, ISortedObservableCollectionAdaptor? adaptor = null) + public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, BindingOptions options) where TObject : notnull where TKey : notnull { @@ -747,7 +820,7 @@ public static IObservable> Bind(this IO var target = new ObservableCollectionExtended(); var result = new ReadOnlyObservableCollection(target); - var updater = adaptor ?? new SortedObservableCollectionAdaptor(resetThreshold, useReplaceForUpdates); + var updater = new SortedObservableCollectionAdaptor(options); readOnlyObservableCollection = result; return source.Bind(target, updater); } @@ -759,25 +832,29 @@ public static IObservable> Bind(this IO /// The type of the key. /// The source. /// The resulting read only observable collection. - /// The number of changes before a reset notification is triggered. + /// The number of changes before a reset event is called on the observable collection. /// Use replace instead of remove / add for updates. NB: Some platforms to not support replace notifications for binding. /// Specify an adaptor to change the algorithm to update the target collection. /// An observable which will emit change sets. /// source. - public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, int resetThreshold = 25, bool useReplaceForUpdates = false, IObservableCollectionAdaptor? adaptor = null) + public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, int resetThreshold = BindingOptions.DefaultResetThreshold, bool useReplaceForUpdates = BindingOptions.DefaultUseReplaceForUpdates, ISortedObservableCollectionAdaptor? adaptor = null) where TObject : notnull where TKey : notnull { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } + if (source is null) throw new ArgumentNullException(nameof(source)); + + // if user has not specified different defaults, use system wide defaults instead. + // This is a hack to retro fit system wide defaults which override the hard coded defaults above + var defaults = DynamicDataOptions.Binding; + var options = resetThreshold == BindingOptions.DefaultResetThreshold && useReplaceForUpdates == BindingOptions.DefaultUseReplaceForUpdates + ? defaults + : defaults with { ResetThreshold = resetThreshold, UseReplaceForUpdates = useReplaceForUpdates }; + + adaptor ??= new SortedObservableCollectionAdaptor(options); var target = new ObservableCollectionExtended(); - var result = new ReadOnlyObservableCollection(target); - var updater = adaptor ?? new ObservableCollectionAdaptor(resetThreshold, useReplaceForUpdates); - readOnlyObservableCollection = result; - return source.Bind(target, updater); + readOnlyObservableCollection = new ReadOnlyObservableCollection(target); + return source.Bind(target, adaptor); } #if SUPPORTS_BINDINGLIST @@ -796,7 +873,7 @@ public static IObservable> Bind(this IO /// or /// targetCollection. /// - public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = 25) + public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = BindingOptions.DefaultResetThreshold) where TObject : notnull where TKey : notnull { @@ -813,10 +890,6 @@ public static IObservable> Bind(this IO return source.Adapt(new BindingListAdaptor(bindingList, resetThreshold)); } -#endif - -#if SUPPORTS_BINDINGLIST - /// /// Binds a clone of the observable change set to the target observable collection. /// @@ -831,7 +904,7 @@ public static IObservable> Bind(this IO /// or /// targetCollection. /// - public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = 25) + public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = BindingOptions.DefaultResetThreshold) where TObject : notnull where TKey : notnull { @@ -3665,12 +3738,14 @@ public static IObservable> OnItemRefreshed changes) + return source.Do(changes => { - changes.Where((Change c) => c.Reason == ChangeReason.Refresh).ForEach(delegate(Change c) + foreach (var change in changes.ToConcreteType()) { - refreshAction2(c.Current); - }); + if (change.Reason != ChangeReason.Refresh) continue; + + refreshAction2(change.Current); + } }); } diff --git a/src/DynamicData/List/ObservableListEx.cs b/src/DynamicData/List/ObservableListEx.cs index b7111ad5b..f2ae426f5 100644 --- a/src/DynamicData/List/ObservableListEx.cs +++ b/src/DynamicData/List/ObservableListEx.cs @@ -285,7 +285,7 @@ public static IObservable> AutoRefreshOnObservable - public static IObservable> Bind(this IObservable> source, IObservableCollection targetCollection, int resetThreshold = 25) + public static IObservable> Bind(this IObservable> source, IObservableCollection targetCollection, int resetThreshold = BindingOptions.DefaultResetThreshold) where T : notnull { if (source is null) @@ -298,7 +298,44 @@ public static IObservable> Bind(this IObservable> throw new ArgumentNullException(nameof(targetCollection)); } - var adaptor = new ObservableCollectionAdaptor(targetCollection, resetThreshold); + // if user has not specified different defaults, use system wide defaults instead. + // This is a hack to retro fit system wide defaults which override the hard coded defaults above + var defaults = DynamicDataOptions.Binding; + + var options = resetThreshold == BindingOptions.DefaultResetThreshold + ? defaults + : defaults with { ResetThreshold = resetThreshold }; + + return source.Bind(targetCollection, options); + } + + /// + /// Binds a clone of the observable change set to the target observable collection. + /// + /// The type of the item. + /// The source. + /// The target collection. + /// The binding options. + /// An observable which emits the change set. + /// + /// source + /// or + /// targetCollection. + /// + public static IObservable> Bind(this IObservable> source, IObservableCollection targetCollection, BindingOptions options) + where T : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (targetCollection is null) + { + throw new ArgumentNullException(nameof(targetCollection)); + } + + var adaptor = new ObservableCollectionAdaptor(targetCollection, options); return source.Adapt(adaptor); } @@ -310,7 +347,33 @@ public static IObservable> Bind(this IObservable> /// The resulting read only observable collection. /// The reset threshold. /// A continuation of the source stream. - public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, int resetThreshold = 25) + public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, int resetThreshold = BindingOptions.DefaultResetThreshold) + where T : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + // if user has not specified different defaults, use system wide defaults instead. + // This is a hack to retro fit system wide defaults which override the hard coded defaults above + var defaults = DynamicDataOptions.Binding; + var options = resetThreshold == BindingOptions.DefaultResetThreshold + ? defaults + : defaults with { ResetThreshold = resetThreshold }; + + return source.Bind(out readOnlyObservableCollection, options); + } + + /// + /// Creates a binding to a readonly observable collection which is specified as an 'out' parameter. + /// + /// The type of the item. + /// The source. + /// The resulting read only observable collection. + /// The binding options. + /// A continuation of the source stream. + public static IObservable> Bind(this IObservable> source, out ReadOnlyObservableCollection readOnlyObservableCollection, BindingOptions options) where T : notnull { if (source is null) @@ -320,7 +383,7 @@ public static IObservable> Bind(this IObservable> var target = new ObservableCollectionExtended(); var result = new ReadOnlyObservableCollection(target); - var adaptor = new ObservableCollectionAdaptor(target, resetThreshold); + var adaptor = new ObservableCollectionAdaptor(target, options); readOnlyObservableCollection = result; return source.Adapt(adaptor); } @@ -340,7 +403,7 @@ public static IObservable> Bind(this IObservable> /// targetCollection. /// /// An observable which emits the change set. - public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = 25) + public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = BindingOptions.DefaultResetThreshold) where T : notnull { if (source is null)