From 36d18e0f34457d4682fea1d68e2e65e8800f87fc Mon Sep 17 00:00:00 2001 From: Glenn <5834289+glennawatson@users.noreply.github.com> Date: Thu, 26 Nov 2020 08:01:53 +0000 Subject: [PATCH] feature: Update to rx 5.0.0 and .net 5 and nullability (#440) --- .github/workflows/ci-build.yml | 50 +- src/Directory.Build.props | 8 +- src/Directory.build.targets | 5 + .../Cache/SourceCache.cs | 5 +- src/DynamicData.Benchmarks/List/GroupAdd.cs | 3 +- .../List/GroupRemove.cs | 3 +- src/DynamicData.Benchmarks/List/SourceList.cs | 5 +- .../AggregationTests/AggregationFixture.cs | 78 +- .../AggregationTests/AverageFixture.cs | 54 +- .../AggregationTests/MaxFixture.cs | 54 +- .../AggregationTests/MinFixture.cs | 53 +- .../AggregationTests/SumFixture.cs | 54 +- src/DynamicData.Tests/AutoRefreshFilter.cs | 37 +- .../Binding/BindingLIstBindListFixture.cs | 59 +- .../Binding/BindingListBindCacheFixture.cs | 67 +- .../BindingListBindCacheSortedFixture.cs | 181 +- .../Binding/BindingListToChangeSetFixture.cs | 86 +- ...eeplyNestedNotifyPropertyChangedFixture.cs | 223 +- .../IObservableListBindCacheFixture.cs | 78 +- .../IObservableListBindCacheSortedFixture.cs | 267 +- .../Binding/IObservableListBindListFixture.cs | 86 +- .../Binding/NotifyPropertyChangedExFixture.cs | 90 +- .../ObservableCollectionBindCacheFixture.cs | 88 +- ...ervableCollectionBindCacheSortedFixture.cs | 222 +- .../ObservableCollectionBindListFixture.cs | 65 +- ...bleCollectionExtendedToChangeSetFixture.cs | 47 +- .../ObservableCollectionToChangeSetFixture.cs | 50 +- ...yObservableCollectionToChangeSetFixture.cs | 99 +- src/DynamicData.Tests/Cache/AndFixture.cs | 54 +- .../Cache/AutoRefreshFixture.cs | 157 +- src/DynamicData.Tests/Cache/BatchFixture.cs | 15 +- src/DynamicData.Tests/Cache/BatchIfFixture.cs | 104 +- .../Cache/BatchIfWithTimeOutFixture.cs | 231 +- .../Cache/BufferInitialFixture.cs | 32 +- .../Cache/ChangesReducerFixture.cs | 115 +- .../Cache/DeferUntilLoadedFixture.cs | 29 +- .../Cache/DisposeManyFixture.cs | 63 +- .../Cache/DistinctFixture.cs | 151 +- .../Cache/DynamicAndFixture.cs | 114 +- .../Cache/DynamicExceptFixture.cs | 108 +- .../Cache/DynamicOrFixture.cs | 111 +- .../Cache/DynamicXorFixture.cs | 109 +- .../Cache/EditDiffFixture.cs | 126 +- ...eObservableToObservableChangeSetFixture.cs | 111 +- src/DynamicData.Tests/Cache/ExceptFixture.cs | 38 +- .../Cache/ExpireAfterFixture.cs | 68 +- .../Cache/FilterControllerFixture.cs | 274 +- src/DynamicData.Tests/Cache/FilterFixture.cs | 107 +- .../Cache/FilterOnPropertyFixture.cs | 68 +- .../Cache/FilterParallelFixture.cs | 72 +- .../Cache/ForEachChangeFixture.cs | 19 +- .../Cache/FromAsyncFixture.cs | 41 +- .../Cache/FullJoinFixture.cs | 312 +- .../Cache/FullJoinManyFixture.cs | 166 +- .../Cache/GroupControllerFixture.cs | 49 +- .../GroupControllerForFilteredItemsFixture.cs | 52 +- src/DynamicData.Tests/Cache/GroupFixture.cs | 355 +- .../Cache/GroupFromDistinctFixture.cs | 32 +- .../Cache/GroupImmutableFixture.cs | 195 +- .../Cache/GroupOnPropertyFixture.cs | 81 +- ...roupOnPropertyWithImmutableStateFixture.cs | 75 +- .../Cache/IgnoreUpdateFixture.cs | 28 +- .../Cache/IncludeUpdateFixture.cs | 28 +- .../Cache/InnerJoinFixture.cs | 297 +- .../Cache/InnerJoinFixtureRaceCondition.cs | 38 +- .../Cache/InnerJoinManyFixture.cs | 161 +- .../Cache/KeyValueCollectionEx.cs | 10 +- .../Cache/LeftJoinFixture.cs | 301 +- .../Cache/LeftJoinManyFixture.cs | 158 +- .../Cache/MergeManyFixture.cs | 82 +- .../Cache/MergeManyItemsFixture.cs | 110 +- .../Cache/MergeManyWithKeyOverloadFixture.cs | 127 +- .../Cache/MonitorStatusFixture.cs | 73 +- .../Cache/ObservableCachePreviewFixture.cs | 148 +- .../Cache/ObservableChangeSetFixture.cs | 187 +- .../ObservableToObservableChangeSetFixture.cs | 168 +- src/DynamicData.Tests/Cache/OnItemFixture.cs | 14 +- src/DynamicData.Tests/Cache/OrFixture.cs | 38 +- src/DynamicData.Tests/Cache/PageFixture.cs | 103 +- .../Cache/QueryWhenChangedFixture.cs | 61 +- .../Cache/RefCountFixture.cs | 106 +- .../Cache/RightJoinFixture.cs | 293 +- .../Cache/RightJoinManyFixture.cs | 162 +- .../Cache/SizeLimitFixture.cs | 86 +- src/DynamicData.Tests/Cache/SortFixture.cs | 1247 ++- .../Cache/SortObservableFixtureFixture.cs | 79 +- .../Cache/SourceCacheFixture.cs | 108 +- .../Cache/SubscribeManyFixture.cs | 86 +- src/DynamicData.Tests/Cache/SwitchFixture.cs | 44 +- .../Cache/TimeExpiryFixture.cs | 60 +- .../Cache/ToObservableChangeSetFixture.cs | 43 +- ...bservableChangeSetFixtureWithCompletion.cs | 27 +- .../Cache/ToSortedCollectionFixture.cs | 91 +- .../Cache/TransformAsyncFixture.cs | 305 +- .../Cache/TransformFixture.cs | 239 +- .../Cache/TransformFixtureParallel.cs | 130 +- .../Cache/TransformManyFixture.cs | 50 +- .../TransformManyObservableCacheFixture.cs | 276 +- .../Cache/TransformManyRefreshFixture.cs | 43 +- .../Cache/TransformManySimpleFixture.cs | 123 +- .../Cache/TransformSafeAsyncFixture.cs | 134 +- .../Cache/TransformSafeFixture.cs | 105 +- .../Cache/TransformSafeParallelFixture.cs | 101 +- .../Cache/TransformTreeFixture.cs | 293 +- .../Cache/TransformTreeWithRefreshFixture.cs | 168 +- .../Cache/TrueForAllFixture.cs | 74 +- .../Cache/TrueForAnyFixture.cs | 59 +- src/DynamicData.Tests/Cache/WatchFixture.cs | 65 +- src/DynamicData.Tests/Cache/WatcherFixture.cs | 86 +- src/DynamicData.Tests/Cache/XorFixture.cs | 44 +- src/DynamicData.Tests/Domain/Animal.cs | 30 +- .../Domain/ParentAndChildren.cs | 47 +- src/DynamicData.Tests/Domain/ParentChild.cs | 7 +- src/DynamicData.Tests/Domain/Person.cs | 72 +- .../Domain/PersonEmployment.cs | 39 +- src/DynamicData.Tests/Domain/PersonObs.cs | 66 +- .../Domain/PersonWithChildren.cs | 25 +- .../Domain/PersonWithEmployment.cs | 6 +- .../Domain/PersonWithFriends.cs | 20 +- .../Domain/PersonWithGender.cs | 40 +- .../Domain/PersonWithRelations.cs | 30 +- src/DynamicData.Tests/Domain/Pet.cs | 8 +- .../Domain/RandomPersonGenerator.cs | 69 +- .../Domain/SelfObservingPerson.cs | 26 +- .../DynamicData.Tests.csproj | 10 +- src/DynamicData.Tests/EnumerableExFixtures.cs | 39 +- .../Kernal/CacheUpdaterFixture.cs | 13 +- .../Kernal/DistinctUpdateFixture.cs | 6 +- src/DynamicData.Tests/Kernal/EnumerableEx.cs | 49 +- .../Kernal/KeyValueFixture.cs | 6 +- src/DynamicData.Tests/Kernal/OptionFixture.cs | 75 +- .../Kernal/SourceUpdaterFixture.cs | 9 +- src/DynamicData.Tests/Kernal/UpdateFixture.cs | 6 +- src/DynamicData.Tests/List/AndFixture.cs | 53 +- .../List/AutoRefreshFixture.cs | 641 +- src/DynamicData.Tests/List/BatchFixture.cs | 24 +- src/DynamicData.Tests/List/BatchIfFixture.cs | 73 +- .../List/BatchIfWithTimeOutFixture.cs | 67 +- src/DynamicData.Tests/List/BufferFixture.cs | 19 +- .../List/BufferInitialFixture.cs | 32 +- src/DynamicData.Tests/List/CastFixture.cs | 28 +- .../List/ChangeAwareListFixture.cs | 156 +- .../List/CloneChangesFixture.cs | 119 +- src/DynamicData.Tests/List/CloneFixture.cs | 78 +- .../List/CreationFixtures.cs | 28 +- .../List/DeferUntilLoadedFixture.cs | 29 +- .../List/DisposeManyFixture.cs | 57 +- .../List/DistinctValuesFixture.cs | 99 +- .../List/DynamicAndFixture.cs | 108 +- .../List/DynamicExceptFixture.cs | 157 +- .../List/DynamicOrFixture.cs | 139 +- .../List/DynamicXOrFixture.cs | 134 +- src/DynamicData.Tests/List/EditDiffFixture.cs | 51 +- src/DynamicData.Tests/List/ExceptFixture.cs | 77 +- .../List/ExpireAfterFixture.cs | 58 +- ...terControllerFixtureWithClearAndReplace.cs | 198 +- .../FilterControllerFixtureWithDiffSet.cs | 170 +- src/DynamicData.Tests/List/FilterFixture.cs | 164 +- .../List/FilterOnObservableFixture.cs | 127 +- .../List/FilterOnPropertyFixture.cs | 87 +- .../List/FilterWithObservable.cs | 273 +- .../List/ForEachChangeFixture.cs | 43 +- .../List/FromAsyncFixture.cs | 36 +- .../List/GroupImmutableFixture.cs | 201 +- src/DynamicData.Tests/List/GroupOnFixture.cs | 45 +- .../List/GroupOnPropertyFixture.cs | 87 +- ...roupOnPropertyWithImmutableStateFixture.cs | 77 +- .../List/MergeManyChangeSetsFixture.cs | 11 +- .../List/MergeManyFixture.cs | 81 +- src/DynamicData.Tests/List/OrFixture.cs | 55 +- src/DynamicData.Tests/List/PageFixture.cs | 150 +- .../List/QueryWhenChangedFixture.cs | 77 +- .../List/RecursiveTransformManyFixture.cs | 46 +- src/DynamicData.Tests/List/RefCountFixture.cs | 88 +- .../List/RemoveManyFixture.cs | 29 +- src/DynamicData.Tests/List/ReverseFixture.cs | 74 +- src/DynamicData.Tests/List/SelectFixture.cs | 99 +- .../List/SizeLimitFixture.cs | 46 +- src/DynamicData.Tests/List/SortFixture.cs | 90 +- .../List/SortMutableFixture.cs | 141 +- .../List/SortPrimitiveFixture.cs | 12 +- .../List/SourceListPreviewFixture.cs | 160 +- .../List/SubscribeManyFixture.cs | 92 +- src/DynamicData.Tests/List/SwitchFixture.cs | 50 +- .../List/ToObservableChangeSetFixture.cs | 44 +- .../List/TransformAsyncFixture.cs | 23 +- .../List/TransformFixture.cs | 128 +- .../List/TransformManyFixture.cs | 190 +- ...ransformManyObservableCollectionFixture.cs | 366 +- .../List/TransformManyProjectionFixture.cs | 201 +- .../List/TransformManyRefreshFixture.cs | 44 +- .../List/VirtualisationFixture.cs | 81 +- src/DynamicData.Tests/List/XOrFixture.cs | 65 +- .../Utilities/SelectManyExtensions.cs | 69 +- .../Aggregation/AggregateEnumerator.cs | 14 +- src/DynamicData/Aggregation/AggregateItem.cs | 36 +- src/DynamicData/Aggregation/AggregateType.cs | 10 +- src/DynamicData/Aggregation/AggregationEx.cs | 140 +- src/DynamicData/Aggregation/Avg.cs | 5 +- src/DynamicData/Aggregation/AvgEx.cs | 426 +- src/DynamicData/Aggregation/CountEx.cs | 72 +- .../Aggregation/IAggregateChangeSet.cs | 8 +- src/DynamicData/Aggregation/MaxEx.cs | 200 +- src/DynamicData/Aggregation/StdDev.cs | 4 +- src/DynamicData/Aggregation/StdDevEx.cs | 217 +- src/DynamicData/Aggregation/SumEx.cs | 351 +- src/DynamicData/Alias/ObservableCacheAlias.cs | 414 +- src/DynamicData/Alias/ObservableListAlias.cs | 114 +- src/DynamicData/Attributes.cs | 4 +- .../Binding/AbstractNotifyPropertyChanged.cs | 56 +- src/DynamicData/Binding/BindingListAdaptor.cs | 50 +- .../Binding/BindingListEventsSuspender.cs | 17 +- src/DynamicData/Binding/BindingListEx.cs | 135 +- src/DynamicData/Binding/ExpressionBuilder.cs | 160 +- src/DynamicData/Binding/IEvaluateAware.cs | 8 +- src/DynamicData/Binding/IIndexAware.cs | 6 +- .../INotifyCollectionChangedSuspender.cs | 12 +- .../Binding/IObservableCollection.cs | 24 +- .../Binding/IObservableCollectionAdaptor.cs | 9 +- src/DynamicData/Binding/IObservableListEx.cs | 133 +- .../ISortedObservableCollectionAdaptor.cs | 9 +- .../Binding/NotifyPropertyChangedEx.cs | 451 +- src/DynamicData/Binding/Observable.cs | 18 + .../Binding/ObservableCollectionAdaptor.cs | 42 +- .../Binding/ObservableCollectionEx.cs | 187 +- .../Binding/ObservableCollectionExtended.cs | 196 +- .../Binding/ObservablePropertyFactory.cs | 74 +- .../Binding/ObservablePropertyFactoryCache.cs | 44 +- .../Binding/ObservablePropertyPart.cs | 10 +- src/DynamicData/Binding/PropertyValue.cs | 110 +- src/DynamicData/Binding/SortDirection.cs | 10 +- src/DynamicData/Binding/SortExpression.cs | 12 +- .../Binding/SortExpressionComparer.cs | 73 +- .../Binding/SortedBindingListAdaptor.cs | 24 +- .../SortedObservableCollectionAdaptor.cs | 24 +- src/DynamicData/Cache/CacheChangeSetEx.cs | 23 + src/DynamicData/Cache/Change.cs | 112 +- src/DynamicData/Cache/ChangeAwareCache.cs | 236 +- src/DynamicData/Cache/ChangeReason.cs | 26 +- src/DynamicData/Cache/ChangeSet.cs | 52 +- src/DynamicData/Cache/DistinctChangeSet.cs | 8 +- src/DynamicData/Cache/GroupChangeSet.cs | 15 +- src/DynamicData/Cache/ICache.cs | 47 +- src/DynamicData/Cache/ICacheUpdater.cs | 100 +- src/DynamicData/Cache/IChangeSet.cs | 7 +- src/DynamicData/Cache/IChangeSetAdaptor.cs | 11 +- src/DynamicData/Cache/IConnectableCache.cs | 45 + src/DynamicData/Cache/IDistinctChangeSet.cs | 11 +- src/DynamicData/Cache/IGroup.cs | 15 +- src/DynamicData/Cache/IGroupChangeSet.cs | 19 +- src/DynamicData/Cache/IGrouping.cs | 32 +- .../Cache/IImmutableGroupChangeSet.cs | 13 +- src/DynamicData/Cache/IIntermediateCache.cs | 8 +- src/DynamicData/Cache/IKey.cs | 18 + src/DynamicData/Cache/IKeyValue.cs | 23 +- src/DynamicData/Cache/IKeyValueCollection.cs | 23 +- src/DynamicData/Cache/IObservableCache.cs | 59 +- src/DynamicData/Cache/IPageRequest.cs | 13 +- src/DynamicData/Cache/IPageResponse.cs | 23 +- src/DynamicData/Cache/IPagedChangeSet.cs | 10 +- src/DynamicData/Cache/IQuery.cs | 35 +- src/DynamicData/Cache/ISortedChangeSet.cs | 12 +- .../Cache/ISortedChangeSetAdaptor.cs | 10 +- src/DynamicData/Cache/ISourceCache.cs | 20 +- src/DynamicData/Cache/ISourceUpdater.cs | 32 +- src/DynamicData/Cache/IVirtualChangeSet.cs | 12 +- src/DynamicData/Cache/IVirtualParameters.cs | 27 - src/DynamicData/Cache/IVirtualRequest.cs | 13 +- src/DynamicData/Cache/IVirtualResponse.cs | 30 + src/DynamicData/Cache/IndexedItem.cs | 66 +- src/DynamicData/Cache/IntermediateCache.cs | 123 +- .../Cache/Internal/AbstractFilter.cs | 59 +- .../Internal/AnonymousObservableCache.cs | 42 +- .../Cache/Internal/AnonymousQuery.cs | 10 +- src/DynamicData/Cache/Internal/AutoRefresh.cs | 66 +- src/DynamicData/Cache/Internal/BatchIf.cs | 177 +- src/DynamicData/Cache/Internal/Cache.cs | 93 +- src/DynamicData/Cache/Internal/CacheEx.cs | 23 +- .../Cache/Internal/CacheUpdater.cs | 250 +- src/DynamicData/Cache/Internal/Cast.cs | 26 +- .../Cache/Internal/ChangesReducer.cs | 6 +- .../Cache/Internal/CombineOperator.cs | 14 +- src/DynamicData/Cache/Internal/Combiner.cs | 92 +- .../Cache/Internal/DeferUntilLoaded.cs | 22 +- .../Cache/Internal/DictionaryExtensions.cs | 4 +- src/DynamicData/Cache/Internal/DisposeMany.cs | 75 +- .../Cache/Internal/DistinctCalculator.cs | 59 +- .../Cache/Internal/DynamicCombiner.cs | 228 +- .../Cache/Internal/DynamicFilter.cs | 100 +- src/DynamicData/Cache/Internal/EditDiff.cs | 46 +- .../Cache/Internal/ExpirableItem.cs | 47 +- src/DynamicData/Cache/Internal/FilterEx.cs | 108 +- .../Cache/Internal/FilterOnProperty.cs | 23 +- .../Cache/Internal/FilteredIndexCalculator.cs | 78 +- src/DynamicData/Cache/Internal/FinallySafe.cs | 66 +- src/DynamicData/Cache/Internal/FullJoin.cs | 202 +- .../Cache/Internal/FullJoinMany.cs | 22 +- .../Cache/Internal/GroupImmutable.cs | 320 - src/DynamicData/Cache/Internal/GroupOn.cs | 366 +- .../Cache/Internal/GroupOnImmutable.cs | 314 + .../Cache/Internal/GroupOnProperty.cs | 54 +- .../GroupOnPropertyWithImmutableState.cs | 50 +- src/DynamicData/Cache/Internal/IFilter.cs | 25 +- .../Cache/Internal/IKeySelector.cs | 16 +- .../Cache/Internal/ImmutableGroup.cs | 57 +- .../Cache/Internal/ImmutableGroupChangeSet.cs | 13 +- .../Cache/Internal/IndexAndNode.cs | 21 +- .../Cache/Internal/IndexCalculator.cs | 229 +- src/DynamicData/Cache/Internal/InnerJoin.cs | 198 +- .../Cache/Internal/InnerJoinMany.cs | 14 +- src/DynamicData/Cache/Internal/KeyComparer.cs | 8 +- src/DynamicData/Cache/Internal/KeySelector.cs | 4 +- .../Cache/Internal/KeySelectorException.cs | 15 +- .../Cache/Internal/KeyValueCollection.cs | 15 +- .../Cache/Internal/KeyValueComparer.cs | 25 +- src/DynamicData/Cache/Internal/LeftJoin.cs | 206 +- .../Cache/Internal/LeftJoinMany.cs | 22 +- .../Cache/Internal/LockFreeObservableCache.cs | 197 +- .../Cache/Internal/ManagedGroup.cs | 67 +- src/DynamicData/Cache/Internal/MergeMany.cs | 22 +- .../Cache/Internal/MergeManyItems.cs | 19 +- .../Cache/Internal/ObservableWithValue.cs | 11 +- src/DynamicData/Cache/Internal/Page.cs | 89 +- .../Cache/Internal/QueryWhenChanged.cs | 64 +- .../Cache/Internal/ReaderWriter.cs | 220 +- src/DynamicData/Cache/Internal/RefCount.cs | 67 +- .../Cache/Internal/RemoveKeyEnumerator.cs | 40 +- src/DynamicData/Cache/Internal/RightJoin.cs | 202 +- .../Cache/Internal/RightJoinMany.cs | 16 +- src/DynamicData/Cache/Internal/SizeExpirer.cs | 52 +- src/DynamicData/Cache/Internal/SizeLimiter.cs | 33 +- src/DynamicData/Cache/Internal/Sort.cs | 170 +- .../Cache/Internal/SpecifiedGrouper.cs | 106 +- .../Cache/Internal/StaticFilter.cs | 28 +- .../Cache/Internal/StatusMonitor.cs | 82 +- .../Cache/Internal/SubscribeMany.cs | 26 +- src/DynamicData/Cache/Internal/Switch.cs | 39 +- src/DynamicData/Cache/Internal/TimeExpirer.cs | 174 +- .../Cache/Internal/ToObservableChangeSet.cs | 194 +- src/DynamicData/Cache/Internal/Transform.cs | 116 +- .../Cache/Internal/TransformAsync.cs | 153 +- .../Cache/Internal/TransformMany.cs | 265 +- .../Internal/TransformWithForcedTransform.cs | 53 +- src/DynamicData/Cache/Internal/TreeBuilder.cs | 401 +- src/DynamicData/Cache/Internal/TrueFor.cs | 38 +- .../{Virtualiser.cs => Virtualise.cs} | 63 +- src/DynamicData/Cache/MissingKeyException.cs | 11 +- src/DynamicData/Cache/Node.cs | 149 +- src/DynamicData/Cache/ObservableCache.cs | 257 +- src/DynamicData/Cache/ObservableCacheEx.cs | 6937 ++++++++--------- src/DynamicData/Cache/PageRequest.cs | 72 +- src/DynamicData/Cache/PageResponse.cs | 72 +- src/DynamicData/Cache/PagedChangeSet.cs | 47 +- src/DynamicData/Cache/SortOptimisations.cs | 15 +- src/DynamicData/Cache/SortReason.cs | 9 +- src/DynamicData/Cache/SortedChangeSet.cs | 35 +- src/DynamicData/Cache/SourceCache.cs | 54 +- src/DynamicData/Cache/SourceCacheEx.cs | 11 +- .../Cache/Tests/ChangeSetAggregator.cs | 48 +- .../Tests/DistinctChangeSetAggregator.cs | 55 +- .../Cache/Tests/PagedChangeSetAggregator.cs | 56 +- .../Cache/Tests/SortedChangeSetAggregator.cs | 48 +- src/DynamicData/Cache/Tests/TestEx.cs | 47 +- .../Cache/Tests/VirtualChangeSetAggregator.cs | 48 +- src/DynamicData/Cache/VirtualChangeSet.cs | 65 +- src/DynamicData/Cache/VirtualRequest.cs | 92 +- src/DynamicData/Cache/VirtualResponse.cs | 69 +- src/DynamicData/Constants.cs | 11 + .../Diagnostics/ChangeStatistics.cs | 118 +- src/DynamicData/Diagnostics/ChangeSummary.cs | 52 +- .../Diagnostics/DiagnosticOperators.cs | 77 +- src/DynamicData/DynamicData.csproj | 6 +- .../DynamicData.csproj.DotSettings | 16 +- src/DynamicData/EnumerableEx.cs | 92 +- .../Experimental/ExperimentalEx.cs | 17 +- .../Experimental/ISubjectWithRefCount.cs | 10 +- src/DynamicData/Experimental/IWatcher.cs | 11 +- .../Experimental/SubjectWithRefCount.cs | 51 +- src/DynamicData/Experimental/Watcher.cs | 109 +- src/DynamicData/IChangeSet.cs | 32 +- src/DynamicData/Kernel/ConnectionStatus.cs | 6 +- src/DynamicData/Kernel/DoubleCheck.cs | 54 - src/DynamicData/Kernel/EnumerableEx.cs | 110 +- src/DynamicData/Kernel/EnumerableIList.cs | 83 +- src/DynamicData/Kernel/EnumeratorIList.cs | 42 + src/DynamicData/Kernel/Error.cs | 39 +- src/DynamicData/Kernel/IEnumerableIList.cs | 22 + src/DynamicData/Kernel/ISupportsCapacity.cs | 22 + src/DynamicData/Kernel/ISupportsCapcity.cs | 12 - src/DynamicData/Kernel/InternalEx.cs | 86 +- src/DynamicData/Kernel/ItemWithIndex.cs | 69 +- src/DynamicData/Kernel/ItemWithValue.cs | 77 +- src/DynamicData/Kernel/OptionElse.cs | 12 +- src/DynamicData/Kernel/OptionExtensions.cs | 251 +- src/DynamicData/Kernel/Optional.cs | 172 +- src/DynamicData/Kernel/ParallelEx.cs | 33 +- .../Kernel/ReadOnlyCollectionLight.cs | 12 +- .../Kernel/ReferenceEqualityComparer.cs | 10 +- src/DynamicData/List/Change.cs | 95 +- src/DynamicData/List/ChangeAwareList.cs | 736 +- .../List/ChangeAwareListWithRefCounts.cs | 35 +- src/DynamicData/List/ChangeSet.cs | 47 +- src/DynamicData/List/ChangeSetEx.cs | 116 +- src/DynamicData/List/ChangeType.cs | 12 +- src/DynamicData/List/IChangeSet.cs | 8 +- src/DynamicData/List/IChangeSetAdaptor.cs | 8 +- src/DynamicData/List/IExtendedList.cs | 35 +- src/DynamicData/List/IGroup.cs | 9 +- src/DynamicData/List/IGrouping.cs | 15 +- src/DynamicData/List/IObservableList.cs | 37 +- src/DynamicData/List/IPageChangeSet.cs | 4 +- src/DynamicData/List/ISourceList.cs | 10 +- src/DynamicData/List/IVirtualChangeSet.cs | 7 +- .../List/Internal/AnonymousObservableList.cs | 16 +- src/DynamicData/List/Internal/AutoRefresh.cs | 79 +- src/DynamicData/List/Internal/BufferIf.cs | 137 +- src/DynamicData/List/Internal/Combiner.cs | 163 +- .../List/Internal/DeferUntilLoaded.cs | 15 +- src/DynamicData/List/Internal/Distinct.cs | 132 +- .../List/Internal/DynamicCombiner.cs | 283 +- src/DynamicData/List/Internal/EditDiff.cs | 30 +- .../List/Internal/ExpirableItem.cs | 43 +- src/DynamicData/List/Internal/ExpireAfter.cs | 128 +- src/DynamicData/List/Internal/Filter.cs | 293 +- .../List/Internal/FilterOnObservable.cs | 177 +- .../List/Internal/FilterOnProperty.cs | 21 +- src/DynamicData/List/Internal/FilterStatic.cs | 91 +- src/DynamicData/List/Internal/Group.cs | 61 +- src/DynamicData/List/Internal/GroupOn.cs | 388 +- .../List/Internal/GroupOnImmutable.cs | 405 +- .../List/Internal/GroupOnProperty.cs | 51 +- .../GroupOnPropertyWithImmutableState.cs | 47 +- .../List/Internal/ImmutableGroup.cs | 40 +- src/DynamicData/List/Internal/LimitSizeTo.cs | 50 + src/DynamicData/List/Internal/MergeMany.cs | 20 +- src/DynamicData/List/Internal/OnBeingAdded.cs | 10 +- .../List/Internal/OnBeingRemoved.cs | 33 +- src/DynamicData/List/Internal/Pager.cs | 106 +- .../List/Internal/QueryWhenChanged.cs | 21 +- src/DynamicData/List/Internal/ReaderWriter.cs | 114 +- src/DynamicData/List/Internal/RefCount.cs | 64 +- .../List/Internal/ReferenceCountTracker.cs | 41 +- src/DynamicData/List/Internal/SizeLimiter.cs | 56 - src/DynamicData/List/Internal/Sort.cs | 269 +- .../List/Internal/SubscribeMany.cs | 18 +- src/DynamicData/List/Internal/Switch.cs | 41 +- .../List/Internal/ToObservableChangeSet.cs | 163 +- .../List/Internal/TransformAsync.cs | 265 +- .../List/Internal/TransformMany.cs | 569 +- src/DynamicData/List/Internal/Transformer.cs | 206 +- .../List/Internal/UnifiedChange.cs | 43 +- src/DynamicData/List/Internal/Virtualiser.cs | 85 +- src/DynamicData/List/ItemChange.cs | 125 +- src/DynamicData/List/Linq/AddKeyEnumerator.cs | 17 +- .../List/Linq/ItemChangeEnumerator.cs | 10 +- .../{ReverseEnumerator.cs => Reverser.cs} | 4 +- .../List/Linq/UnifiedChangeEnumerator.cs | 8 +- .../List/Linq/WithoutIndexEnumerator.cs | 10 +- src/DynamicData/List/ListChangeReason.cs | 21 +- src/DynamicData/List/ListEx.cs | 927 ++- src/DynamicData/List/ListFilterPolicy.cs | 7 +- src/DynamicData/List/ObservableListEx.cs | 2780 +++---- src/DynamicData/List/PageChangeSet.cs | 23 +- src/DynamicData/List/RangeChange.cs | 82 +- src/DynamicData/List/SortException.cs | 19 +- src/DynamicData/List/SortOptions.cs | 9 +- src/DynamicData/List/SourceList.cs | 209 +- .../List/SourceListEditConvenienceEx.cs | 188 +- src/DynamicData/List/SourceListEx.cs | 19 +- .../List/Tests/ChangeSetAggregator.cs | 44 +- .../List/Tests/{TestEx.cs => ListTextEx.cs} | 13 +- .../List/UnspecifiedIndexException.cs | 9 +- src/DynamicData/List/VirtualChangeSet.cs | 22 +- src/DynamicData/ObservableChangeSet.cs | 532 +- src/DynamicData/ObsoleteEx.cs | 12 +- .../{PLinqFilteredUpdater.cs => PFilter.cs} | 48 +- .../Platforms/net45/PSubscribeMany.cs | 30 +- src/DynamicData/Platforms/net45/PTransform.cs | 38 +- src/DynamicData/Platforms/net45/ParallelEx.cs | 36 +- .../Platforms/net45/ParallelOperators.cs | 174 +- .../Platforms/net45/ParallelType.cs | 16 +- .../Platforms/net45/ParallelisationOptions.cs | 33 +- src/DynamicData/Properties/Annotations.cs | 1060 --- src/analyzers.ruleset | 7 +- src/stylecop.json | 44 + 485 files changed, 26270 insertions(+), 27875 deletions(-) create mode 100644 src/DynamicData/Binding/Observable.cs create mode 100644 src/DynamicData/Cache/CacheChangeSetEx.cs create mode 100644 src/DynamicData/Cache/IConnectableCache.cs create mode 100644 src/DynamicData/Cache/IKey.cs delete mode 100644 src/DynamicData/Cache/IVirtualParameters.cs create mode 100644 src/DynamicData/Cache/IVirtualResponse.cs delete mode 100644 src/DynamicData/Cache/Internal/GroupImmutable.cs create mode 100644 src/DynamicData/Cache/Internal/GroupOnImmutable.cs rename src/DynamicData/Cache/Internal/{Virtualiser.cs => Virtualise.cs} (65%) create mode 100644 src/DynamicData/Constants.cs delete mode 100644 src/DynamicData/Kernel/DoubleCheck.cs create mode 100644 src/DynamicData/Kernel/EnumeratorIList.cs create mode 100644 src/DynamicData/Kernel/IEnumerableIList.cs create mode 100644 src/DynamicData/Kernel/ISupportsCapacity.cs delete mode 100644 src/DynamicData/Kernel/ISupportsCapcity.cs create mode 100644 src/DynamicData/List/Internal/LimitSizeTo.cs delete mode 100644 src/DynamicData/List/Internal/SizeLimiter.cs rename src/DynamicData/List/Linq/{ReverseEnumerator.cs => Reverser.cs} (97%) rename src/DynamicData/List/Tests/{TestEx.cs => ListTextEx.cs} (61%) rename src/DynamicData/Platforms/net45/{PLinqFilteredUpdater.cs => PFilter.cs} (76%) delete mode 100644 src/DynamicData/Properties/Annotations.cs create mode 100644 src/stylecop.json diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 52dadda62..26c19f769 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -27,6 +27,18 @@ jobs: with: dotnet-version: 3.1.x + - name: Install .NET 5 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v1.0.2 + + - name: Update VS2019 + shell: powershell + run: Start-Process -Wait -PassThru -FilePath "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vs_installer.exe" -ArgumentList "update --passive --norestart --installpath ""C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise""" + - name: NBGV id: nbgv uses: dotnet/nbgv@master @@ -37,29 +49,28 @@ jobs: run: dotnet restore working-directory: src - - name: Add MSBuild to PATH - uses: microsoft/setup-msbuild@v1.0.2 - - name: Build - run: msbuild /t:build,pack /maxcpucount /p:NoPackageAnalysis=true /verbosity:minimal /p:Configuration=${{ env.configuration }} + run: msbuild /t:build,pack /nowarn:MSB4011 /maxcpucount /p:NoPackageAnalysis=true /verbosity:minimal /p:Configuration=${{ env.configuration }} working-directory: src - - name: Install Report Generator - run: dotnet tool install --global dotnet-reportgenerator-globaltool - - - name: Run Unit Tests - run: dotnet test --no-build /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput="../../artifacts/coverage/coverage.xml" /p:Include="[${{ env.productNamespacePrefix}}*]*" /p:Exclude="[${{ env.productNamespacePrefix}}*Tests.*]*" - working-directory: src - - - name: Generate Coverage Report - run: reportgenerator -reports:"coverage.*.xml" -targetdir:report-output - working-directory: artifacts/coverage + - name: Run Unit Tests and Generate Coverage + uses: glennawatson/coverlet-msbuild@v1 + with: + project-files: '**/*Tests*.csproj' + no-build: true + exclude-filter: '[${{env.productNamespacePrefix}}.*.Tests.*]*' + include-filter: '[${{env.productNamespacePrefix}}*]*' + output-format: cobertura + output: '../../artifacts/' + configuration: ${{ env.configuration }} - name: Upload Code Coverage - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - directory: artifacts/coverage + shell: bash + run: | + echo $PWD + bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -t ${{ env.CODECOV_TOKEN }} -s '$PWD/artifacts' -f '*.xml' + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Create NuGet Artifacts uses: actions/upload-artifact@master @@ -80,7 +91,7 @@ jobs: - name: Download NuGet Packages uses: actions/download-artifact@v2 with: - name: nuget + name: nuget - name: Changelog uses: glennawatson/ChangeLog@v1 @@ -102,4 +113,3 @@ jobs: SOURCE_URL: https://api.nuget.org/v3/index.json run: | dotnet nuget push -s ${{ env.SOURCE_URL }} -k ${{ env.NUGET_AUTH_TOKEN }} **/*.nupkg - diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1903f52f9..8872086b6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -27,6 +27,7 @@ true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + CS8600;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8623;CS8624;CS8625;CS8626;CS8627;CS8628;CS8629;CS8630;CS8634;CS8766;CS8767 @@ -54,9 +55,12 @@ - - + + + + + diff --git a/src/Directory.build.targets b/src/Directory.build.targets index 4b8aa2f3b..37f649997 100644 --- a/src/Directory.build.targets +++ b/src/Directory.build.targets @@ -18,4 +18,9 @@ $(DefineConstants);NETCOREAPP;P_LINQ;SUPPORTS_BINDINGLIST + + + $(DefineConstants);NETSTANDARD;PORTABLE;P_LINQ;SUPPORTS_BINDINGLIST + + diff --git a/src/DynamicData.Benchmarks/Cache/SourceCache.cs b/src/DynamicData.Benchmarks/Cache/SourceCache.cs index 1d695aca5..ecf4265ab 100644 --- a/src/DynamicData.Benchmarks/Cache/SourceCache.cs +++ b/src/DynamicData.Benchmarks/Cache/SourceCache.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for full license information. using System.Linq; + using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; @@ -24,12 +25,12 @@ public BenchmarkItem(int id) public class SourceCache { private SourceCache _cache; - private BenchmarkItem[] _items = Enumerable.Range(1,100).Select(i=> new BenchmarkItem(i)).ToArray(); + private BenchmarkItem[] _items = Enumerable.Range(1, 100).Select(i => new BenchmarkItem(i)).ToArray(); [GlobalSetup] public void Setup() { - _cache = new SourceCache(i=> i.Id); + _cache = new SourceCache(i => i.Id); } [Params(1, 100, 1_000, 10_000, 100_000)] diff --git a/src/DynamicData.Benchmarks/List/GroupAdd.cs b/src/DynamicData.Benchmarks/List/GroupAdd.cs index 1377c6f81..eb013336e 100644 --- a/src/DynamicData.Benchmarks/List/GroupAdd.cs +++ b/src/DynamicData.Benchmarks/List/GroupAdd.cs @@ -4,6 +4,7 @@ using System; using System.Linq; + using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; @@ -16,7 +17,7 @@ public class GroupAdd { private IDisposable _groupSubscription; private SourceList _sourceList; - private int[] _items = Enumerable.Range(1,100).ToArray(); + private int[] _items = Enumerable.Range(1, 100).ToArray(); [Params(1, 100, 1_000, 10_000, 100_000)] public int N; diff --git a/src/DynamicData.Benchmarks/List/GroupRemove.cs b/src/DynamicData.Benchmarks/List/GroupRemove.cs index 2d0ca301e..c362b903b 100644 --- a/src/DynamicData.Benchmarks/List/GroupRemove.cs +++ b/src/DynamicData.Benchmarks/List/GroupRemove.cs @@ -4,6 +4,7 @@ using System; using System.Linq; + using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; @@ -47,7 +48,7 @@ public void Teardown() public void Remove() => _sourceList.RemoveAt(_items[0]); [Benchmark] - public void RemoveRange() => _sourceList.RemoveRange(40,20); + public void RemoveRange() => _sourceList.RemoveRange(40, 20); [Benchmark] public void Clear() => _sourceList.Clear(); diff --git a/src/DynamicData.Benchmarks/List/SourceList.cs b/src/DynamicData.Benchmarks/List/SourceList.cs index c2c5292e0..2ed4100d1 100644 --- a/src/DynamicData.Benchmarks/List/SourceList.cs +++ b/src/DynamicData.Benchmarks/List/SourceList.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for full license information. using System.Linq; + using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; @@ -13,7 +14,7 @@ namespace DynamicData.Benchmarks.List [MarkdownExporterAttribute.GitHub] public class SourceList { - private SourceList _sourceList; + private SourceList _sourceList; private string[] _items; [Params(1, 100, 1_000, 10_000, 100_000)] @@ -36,6 +37,6 @@ public void SetupIteration() public void AddRange() => _sourceList.AddRange(_items); [Benchmark] - public void Insert() => _sourceList.InsertRange(_items,0); + public void Insert() => _sourceList.InsertRange(_items, 0); } } \ No newline at end of file diff --git a/src/DynamicData.Tests/AggregationTests/AggregationFixture.cs b/src/DynamicData.Tests/AggregationTests/AggregationFixture.cs index e8e6dafea..24462c1ba 100644 --- a/src/DynamicData.Tests/AggregationTests/AggregationFixture.cs +++ b/src/DynamicData.Tests/AggregationTests/AggregationFixture.cs @@ -1,19 +1,22 @@ using System; using System.Reactive.Linq; + using DynamicData.Aggregation; using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.AggregationTests { - - public class AggregationFixture: IDisposable + public class AggregationFixture : IDisposable { - private readonly SourceCache _source; private readonly IObservable _accumulator; + private readonly SourceCache _source; + /// /// Initialises this instance. /// @@ -21,28 +24,24 @@ public AggregationFixture() { _source = new SourceCache(p => p.Name); - _accumulator = _source.Connect() - .ForAggregation() - .Scan(0, (current, items) => - { - items.ForEach(x => - { - if (x.Type == AggregateType.Add) - { - current = current + x.Item.Age; - } - else - { - current = current - x.Item.Age; - } - }); - return current; - }); - } - - public void Dispose() - { - _source.Dispose(); + _accumulator = _source.Connect().ForAggregation().Scan( + 0, + (current, items) => + { + items.ForEach( + x => + { + if (x.Type == AggregateType.Add) + { + current += x.Item.Age; + } + else + { + current -= x.Item.Age; + } + }); + return current; + }); } [Fact] @@ -51,11 +50,12 @@ public void CanAccumulate() int latest = 0; int counter = 0; - var accumulator = _accumulator.Subscribe(value => - { - latest = value; - counter++; - }); + var accumulator = _accumulator.Subscribe( + value => + { + latest = value; + counter++; + }); _source.AddOrUpdate(new Person("A", 10)); _source.AddOrUpdate(new Person("B", 20)); @@ -74,11 +74,12 @@ public void CanHandleUpdatedItem() int latest = 0; int counter = 0; - var accumulator = _accumulator.Subscribe(value => - { - latest = value; - counter++; - }); + var accumulator = _accumulator.Subscribe( + value => + { + latest = value; + counter++; + }); _source.AddOrUpdate(new Person("A", 10)); _source.AddOrUpdate(new Person("A", 15)); @@ -87,5 +88,10 @@ public void CanHandleUpdatedItem() latest.Should().Be(15, "Accumulated value should be 60"); accumulator.Dispose(); } + + public void Dispose() + { + _source.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/AggregationTests/AverageFixture.cs b/src/DynamicData.Tests/AggregationTests/AverageFixture.cs index 46c5ef5a6..53c9d620c 100644 --- a/src/DynamicData.Tests/AggregationTests/AverageFixture.cs +++ b/src/DynamicData.Tests/AggregationTests/AverageFixture.cs @@ -1,13 +1,15 @@ using System; + using DynamicData.Aggregation; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.AggregationTests { - - public class AverageFixture: IDisposable + public class AverageFixture : IDisposable { private readonly SourceCache _source; @@ -16,19 +18,12 @@ public AverageFixture() _source = new SourceCache(p => p.Name); } - public void Dispose() - { - _source.Dispose(); - } - [Fact] public void AddedItemsContributeToSum() { double avg = 0; - var accumulator = _source.Connect() - .Avg(p => p.Age) - .Subscribe(x => avg = x); + var accumulator = _source.Connect().Avg(p => p.Age).Subscribe(x => avg = x); _source.AddOrUpdate(new Person("A", 10)); _source.AddOrUpdate(new Person("B", 20)); @@ -39,22 +34,9 @@ public void AddedItemsContributeToSum() accumulator.Dispose(); } - [Fact] - public void RemoveProduceCorrectResult() + public void Dispose() { - double avg = 0; - - var accumulator = _source.Connect() - .Avg(p => p.Age) - .Subscribe(x => avg = x); - - _source.AddOrUpdate(new Person("A", 10)); - _source.AddOrUpdate(new Person("B", 20)); - _source.AddOrUpdate(new Person("C", 30)); - - _source.Remove("A"); - avg.Should().Be(25, "Average value should be 25 after remove"); - accumulator.Dispose(); + _source.Dispose(); } [Fact] @@ -64,10 +46,7 @@ public void InlineChangeReEvaluatesTotals() var somepropChanged = _source.Connect().WhenValueChanged(p => p.Age); - var accumulator = _source.Connect() - .Avg(p => p.Age) - .InvalidateWhen(somepropChanged) - .Subscribe(x => avg = x); + var accumulator = _source.Connect().Avg(p => p.Age).InvalidateWhen(somepropChanged).Subscribe(x => avg = x); var personb = new Person("B", 5); _source.AddOrUpdate(new Person("A", 10)); @@ -82,5 +61,20 @@ public void InlineChangeReEvaluatesTotals() accumulator.Dispose(); } + [Fact] + public void RemoveProduceCorrectResult() + { + double avg = 0; + + var accumulator = _source.Connect().Avg(p => p.Age).Subscribe(x => avg = x); + + _source.AddOrUpdate(new Person("A", 10)); + _source.AddOrUpdate(new Person("B", 20)); + _source.AddOrUpdate(new Person("C", 30)); + + _source.Remove("A"); + avg.Should().Be(25, "Average value should be 25 after remove"); + accumulator.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/AggregationTests/MaxFixture.cs b/src/DynamicData.Tests/AggregationTests/MaxFixture.cs index 95a6f8326..04880c5e6 100644 --- a/src/DynamicData.Tests/AggregationTests/MaxFixture.cs +++ b/src/DynamicData.Tests/AggregationTests/MaxFixture.cs @@ -1,13 +1,15 @@ using System; + using DynamicData.Aggregation; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.AggregationTests { - - public class MaxFixture: IDisposable + public class MaxFixture : IDisposable { private readonly SourceCache _source; @@ -16,19 +18,12 @@ public MaxFixture() _source = new SourceCache(p => p.Name); } - public void Dispose() - { - _source.Dispose(); - } - [Fact] public void AddItems() { var result = 0; - var accumulator = _source.Connect() - .Maximum(p => p.Age) - .Subscribe(x => result = x); + var accumulator = _source.Connect().Maximum(p => p.Age).Subscribe(x => result = x); _source.AddOrUpdate(new Person("A", 10)); _source.AddOrUpdate(new Person("B", 20)); @@ -39,22 +34,9 @@ public void AddItems() accumulator.Dispose(); } - [Fact] - public void RemoveItems() + public void Dispose() { - var result = 0; - - var accumulator = _source.Connect() - .Maximum(p => p.Age) - .Subscribe(x => result = x); - - _source.AddOrUpdate(new Person("A", 10)); - _source.AddOrUpdate(new Person("B", 20)); - _source.AddOrUpdate(new Person("C", 30)); - - _source.Remove("C"); - result.Should().Be(20, "Max value should be 20 after remove"); - accumulator.Dispose(); + _source.Dispose(); } [Fact] @@ -64,10 +46,7 @@ public void InlineChangeReEvaluatesTotals() var somepropChanged = _source.Connect().WhenValueChanged(p => p.Age); - var accumulator = _source.Connect() - .Maximum(p => p.Age) - .InvalidateWhen(somepropChanged) - .Subscribe(x => max = x); + var accumulator = _source.Connect().Maximum(p => p.Age).InvalidateWhen(somepropChanged).Subscribe(x => max = x); var personc = new Person("C", 5); _source.AddOrUpdate(new Person("A", 10)); @@ -82,5 +61,20 @@ public void InlineChangeReEvaluatesTotals() accumulator.Dispose(); } + [Fact] + public void RemoveItems() + { + var result = 0; + + var accumulator = _source.Connect().Maximum(p => p.Age).Subscribe(x => result = x); + + _source.AddOrUpdate(new Person("A", 10)); + _source.AddOrUpdate(new Person("B", 20)); + _source.AddOrUpdate(new Person("C", 30)); + + _source.Remove("C"); + result.Should().Be(20, "Max value should be 20 after remove"); + accumulator.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/AggregationTests/MinFixture.cs b/src/DynamicData.Tests/AggregationTests/MinFixture.cs index b29e3a2d5..f42927ef7 100644 --- a/src/DynamicData.Tests/AggregationTests/MinFixture.cs +++ b/src/DynamicData.Tests/AggregationTests/MinFixture.cs @@ -1,12 +1,15 @@ using System; + using DynamicData.Aggregation; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.AggregationTests { - public class MinFixture: IDisposable + public class MinFixture : IDisposable { private readonly SourceCache _source; @@ -15,19 +18,12 @@ public MinFixture() _source = new SourceCache(p => p.Name); } - public void Dispose() - { - _source.Dispose(); - } - [Fact] public void AddedItemsContributeToSum() { var result = 0; - var accumulator = _source.Connect() - .Minimum(p => p.Age) - .Subscribe(x => result = x); + var accumulator = _source.Connect().Minimum(p => p.Age).Subscribe(x => result = x); _source.AddOrUpdate(new Person("A", 10)); _source.AddOrUpdate(new Person("B", 20)); @@ -38,22 +34,9 @@ public void AddedItemsContributeToSum() accumulator.Dispose(); } - [Fact] - public void RemoveProduceCorrectResult() + public void Dispose() { - var result = 0; - - var accumulator = _source.Connect() - .Minimum(p => p.Age) - .Subscribe(x => result = x); - - _source.AddOrUpdate(new Person("A", 10)); - _source.AddOrUpdate(new Person("B", 20)); - _source.AddOrUpdate(new Person("C", 30)); - - _source.Remove("A"); - result.Should().Be(20, "Min value should be 20 after remove"); - accumulator.Dispose(); + _source.Dispose(); } [Fact] @@ -63,10 +46,7 @@ public void InlineChangeReEvaluatesTotals() var somepropChanged = _source.Connect().WhenValueChanged(p => p.Age); - var accumulator = _source.Connect() - .Minimum(p => p.Age) - .InvalidateWhen(somepropChanged) - .Subscribe(x => min = x); + var accumulator = _source.Connect().Minimum(p => p.Age).InvalidateWhen(somepropChanged).Subscribe(x => min = x); var personc = new Person("C", 5); _source.AddOrUpdate(new Person("A", 10)); @@ -82,5 +62,20 @@ public void InlineChangeReEvaluatesTotals() accumulator.Dispose(); } + [Fact] + public void RemoveProduceCorrectResult() + { + var result = 0; + + var accumulator = _source.Connect().Minimum(p => p.Age).Subscribe(x => result = x); + + _source.AddOrUpdate(new Person("A", 10)); + _source.AddOrUpdate(new Person("B", 20)); + _source.AddOrUpdate(new Person("C", 30)); + + _source.Remove("A"); + result.Should().Be(20, "Min value should be 20 after remove"); + accumulator.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/AggregationTests/SumFixture.cs b/src/DynamicData.Tests/AggregationTests/SumFixture.cs index ae822bccd..cd2b7ff7b 100644 --- a/src/DynamicData.Tests/AggregationTests/SumFixture.cs +++ b/src/DynamicData.Tests/AggregationTests/SumFixture.cs @@ -1,13 +1,15 @@ using System; + using DynamicData.Aggregation; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.AggregationTests { - - public class SumFixture: IDisposable + public class SumFixture : IDisposable { private readonly SourceCache _source; @@ -16,19 +18,12 @@ public SumFixture() _source = new SourceCache(p => p.Name); } - public void Dispose() - { - _source.Dispose(); - } - [Fact] public void AddedItemsContributeToSum() { int sum = 0; - var accumulator = _source.Connect() - .Sum(p => p.Age) - .Subscribe(x => sum = x); + var accumulator = _source.Connect().Sum(p => p.Age).Subscribe(x => sum = x); _source.AddOrUpdate(new Person("A", 10)); _source.AddOrUpdate(new Person("B", 20)); @@ -39,22 +34,9 @@ public void AddedItemsContributeToSum() accumulator.Dispose(); } - [Fact] - public void RemoveProduceCorrectResult() + public void Dispose() { - int sum = 0; - - var accumulator = _source.Connect() - .Sum(p => p.Age) - .Subscribe(x => sum = x); - - _source.AddOrUpdate(new Person("A", 10)); - _source.AddOrUpdate(new Person("B", 20)); - _source.AddOrUpdate(new Person("C", 30)); - - _source.Remove("A"); - sum.Should().Be(50, "Accumulated value should be 50 after remove"); - accumulator.Dispose(); + _source.Dispose(); } [Fact] @@ -64,10 +46,7 @@ public void InlineChangeReEvaluatesTotals() var somepropChanged = _source.Connect().WhenValueChanged(p => p.Age); - var accumulator = _source.Connect() - .Sum(p => p.Age) - .InvalidateWhen(somepropChanged) - .Subscribe(x => sum = x); + var accumulator = _source.Connect().Sum(p => p.Age).InvalidateWhen(somepropChanged).Subscribe(x => sum = x); var personb = new Person("B", 5); _source.AddOrUpdate(new Person("A", 10)); @@ -82,5 +61,20 @@ public void InlineChangeReEvaluatesTotals() accumulator.Dispose(); } + [Fact] + public void RemoveProduceCorrectResult() + { + int sum = 0; + + var accumulator = _source.Connect().Sum(p => p.Age).Subscribe(x => sum = x); + + _source.AddOrUpdate(new Person("A", 10)); + _source.AddOrUpdate(new Person("B", 20)); + _source.AddOrUpdate(new Person("C", 30)); + + _source.Remove("A"); + sum.Should().Be(50, "Accumulated value should be 50 after remove"); + accumulator.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/AutoRefreshFilter.cs b/src/DynamicData.Tests/AutoRefreshFilter.cs index dda051770..ae14237ff 100644 --- a/src/DynamicData.Tests/AutoRefreshFilter.cs +++ b/src/DynamicData.Tests/AutoRefreshFilter.cs @@ -1,6 +1,8 @@ using System; using System.ComponentModel; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests @@ -16,32 +18,37 @@ public void Test() var i3 = new Item("I3"); var obsList = new SourceList(); - obsList.AddRange(new[] {a0, i1, i2, i3}); + obsList.AddRange(new[] { a0, i1, i2, i3 }); - var obsListDerived = obsList.Connect() - .AutoRefresh(x => x.Name) - .Filter(x => x.Name.Contains("I")) - .AsObservableList(); + var obsListDerived = obsList.Connect().AutoRefresh(x => x.Name).Filter(x => x.Name.Contains("I")).AsObservableList(); obsListDerived.Count.Should().Be(3); - obsListDerived.Items.Should().BeEquivalentTo(new [] {i1, i2, i3}); + obsListDerived.Items.Should().BeEquivalentTo(i1, i2, i3); i1.Name = "X2"; obsListDerived.Count.Should().Be(2); - obsListDerived.Items.Should().BeEquivalentTo(new[] {i2, i3}); + obsListDerived.Items.Should().BeEquivalentTo(i2, i3); a0.Name = "I0"; obsListDerived.Count.Should().Be(3); - obsListDerived.Items.Should().BeEquivalentTo(new[] {a0, i2, i3}); + obsListDerived.Items.Should().BeEquivalentTo(a0, i2, i3); } } public class Item : INotifyPropertyChanged { - public Guid Id { get; } - private string _name; + public Item(string name) + { + Id = Guid.NewGuid(); + _name = name; + } + + public event PropertyChangedEventHandler? PropertyChanged; + + public Guid Id { get; } + public string Name { get => _name; @@ -51,13 +58,5 @@ public string Name PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } - - public Item(string name) - { - Id = Guid.NewGuid(); - Name = name; - } - - public event PropertyChangedEventHandler PropertyChanged; } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/BindingLIstBindListFixture.cs b/src/DynamicData.Tests/Binding/BindingLIstBindListFixture.cs index d0167f700..86d129d3f 100644 --- a/src/DynamicData.Tests/Binding/BindingLIstBindListFixture.cs +++ b/src/DynamicData.Tests/Binding/BindingLIstBindListFixture.cs @@ -3,19 +3,24 @@ using System; using System.ComponentModel; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - public class BindingLIstBindListFixture : IDisposable { + private readonly IDisposable _binder; + private readonly BindingList _collection; + + private readonly RandomPersonGenerator _generator = new(); + private readonly SourceList _source; - private readonly IDisposable _binder; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); public BindingLIstBindListFixture() { @@ -24,10 +29,14 @@ public BindingLIstBindListFixture() _binder = _source.Connect().Bind(_collection).Subscribe(); } - public void Dispose() + [Fact] + public void AddRange() { - _binder.Dispose(); - _source.Dispose(); + var people = _generator.Take(100).ToList(); + _source.AddRange(people); + + _collection.Count.Should().Be(100, "Should be 100 items in the collection"); + _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); } [Fact] @@ -41,15 +50,18 @@ public void AddToSourceAddsToDestination() } [Fact] - public void UpdateToSourceUpdatesTheDestination() + public void Clear() { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.Add(person); - _source.Replace(person, personUpdated); + var people = _generator.Take(100).ToList(); + _source.AddRange(people); + _source.Clear(); + _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + } - _collection.Count.Should().Be(1, "Should be 1 item in the collection"); - _collection.First().Should().Be(personUpdated, "Should be updated person"); + public void Dispose() + { + _binder.Dispose(); + _source.Dispose(); } [Fact] @@ -63,22 +75,15 @@ public void RemoveSourceRemovesFromTheDestination() } [Fact] - public void AddRange() + public void UpdateToSourceUpdatesTheDestination() { - var people = _generator.Take(100).ToList(); - _source.AddRange(people); - - _collection.Count.Should().Be(100, "Should be 100 items in the collection"); - _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); - } + var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); + _source.Add(person); + _source.Replace(person, personUpdated); - [Fact] - public void Clear() - { - var people = _generator.Take(100).ToList(); - _source.AddRange(people); - _source.Clear(); - _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + _collection.Count.Should().Be(1, "Should be 1 item in the collection"); + _collection.First().Should().Be(personUpdated, "Should be updated person"); } } } diff --git a/src/DynamicData.Tests/Binding/BindingListBindCacheFixture.cs b/src/DynamicData.Tests/Binding/BindingListBindCacheFixture.cs index 713c5db5e..e45c55516 100644 --- a/src/DynamicData.Tests/Binding/BindingListBindCacheFixture.cs +++ b/src/DynamicData.Tests/Binding/BindingListBindCacheFixture.cs @@ -3,19 +3,24 @@ using System; using System.ComponentModel; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - public class BindingListCacheFixture : IDisposable { - private readonly BindingList _collection = new BindingList(); - private readonly ISourceCache _source; private readonly IDisposable _binder; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly BindingList _collection = new(); + + private readonly RandomPersonGenerator _generator = new(); + + private readonly ISourceCache _source; public BindingListCacheFixture() { @@ -23,12 +28,6 @@ public BindingListCacheFixture() _binder = _source.Connect().Bind(_collection).Subscribe(); } - public void Dispose() - { - _binder.Dispose(); - _source.Dispose(); - } - [Fact] public void AddToSourceAddsToDestination() { @@ -40,15 +39,28 @@ public void AddToSourceAddsToDestination() } [Fact] - public void UpdateToSourceUpdatesTheDestination() + public void BatchAdd() { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.AddOrUpdate(person); - _source.AddOrUpdate(personUpdated); + var people = _generator.Take(100).ToList(); + _source.AddOrUpdate(people); - _collection.Count.Should().Be(1, "Should be 1 item in the collection"); - _collection.First().Should().Be(personUpdated, "Should be updated person"); + _collection.Count.Should().Be(100, "Should be 100 items in the collection"); + _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); + } + + [Fact] + public void BatchRemove() + { + var people = _generator.Take(100).ToList(); + _source.AddOrUpdate(people); + _source.Clear(); + _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + } + + public void Dispose() + { + _binder.Dispose(); + _source.Dispose(); } [Fact] @@ -62,22 +74,15 @@ public void RemoveSourceRemovesFromTheDestination() } [Fact] - public void BatchAdd() + public void UpdateToSourceUpdatesTheDestination() { - var people = _generator.Take(100).ToList(); - _source.AddOrUpdate(people); - - _collection.Count.Should().Be(100, "Should be 100 items in the collection"); - _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); - } + var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); + _source.AddOrUpdate(person); + _source.AddOrUpdate(personUpdated); - [Fact] - public void BatchRemove() - { - var people = _generator.Take(100).ToList(); - _source.AddOrUpdate(people); - _source.Clear(); - _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + _collection.Count.Should().Be(1, "Should be 1 item in the collection"); + _collection.First().Should().Be(personUpdated, "Should be updated person"); } } } diff --git a/src/DynamicData.Tests/Binding/BindingListBindCacheSortedFixture.cs b/src/DynamicData.Tests/Binding/BindingListBindCacheSortedFixture.cs index a7394f1f3..db2080220 100644 --- a/src/DynamicData.Tests/Binding/BindingListBindCacheSortedFixture.cs +++ b/src/DynamicData.Tests/Binding/BindingListBindCacheSortedFixture.cs @@ -4,36 +4,33 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - public class BindingListBindCacheSortedFixture : IDisposable { - private readonly BindingList _collection; - private readonly ISourceCache _source; private readonly IDisposable _binder; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly BindingList _collection; + private readonly IComparer _comparer = SortExpressionComparer.Ascending(p => p.Name); + private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly ISourceCache _source; + public BindingListBindCacheSortedFixture() { _collection = new BindingList(); - _source = new SourceCache(p => p.Name); - _binder = _source.Connect() - .Sort(_comparer, resetThreshold: 25) - .Bind(_collection) - .Subscribe(); - } - - public void Dispose() - { - _binder.Dispose(); - _source.Dispose(); + _source = new SourceCache(p => p.Name); + _binder = _source.Connect().Sort(_comparer, resetThreshold: 25).Bind(_collection).Subscribe(); } [Fact] @@ -46,28 +43,6 @@ public void AddToSourceAddsToDestination() _collection.First().Should().Be(person, "Should be same person"); } - [Fact] - public void UpdateToSourceUpdatesTheDestination() - { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.AddOrUpdate(person); - _source.AddOrUpdate(personUpdated); - - _collection.Count.Should().Be(1, "Should be 1 item in the collection"); - _collection.First().Should().Be(personUpdated, "Should be updated person"); - } - - [Fact] - public void RemoveSourceRemovesFromTheDestination() - { - var person = new Person("Adult1", 50); - _source.AddOrUpdate(person); - _source.Remove(person); - - _collection.Count.Should().Be(0, "Should be 1 item in the collection"); - } - [Fact] public void BatchAdd() { @@ -95,6 +70,12 @@ public void CollectionIsInSortOrder() sorted.Should().BeEquivalentTo(_collection.ToList()); } + public void Dispose() + { + _binder.Dispose(); + _source.Dispose(); + } + [Fact] public void LargeUpdateInvokesAReset() { @@ -103,15 +84,25 @@ public void LargeUpdateInvokesAReset() bool invoked = false; _collection.ListChanged += (sender, e) => - { - invoked = true; - e.ListChangedType.Should().Be(ListChangedType.Reset); - }; + { + invoked = true; + e.ListChangedType.Should().Be(ListChangedType.Reset); + }; _source.AddOrUpdate(_generator.Take(100)); invoked.Should().BeTrue(); } + [Fact] + public void RemoveSourceRemovesFromTheDestination() + { + var person = new Person("Adult1", 50); + _source.AddOrUpdate(person); + _source.Remove(person); + + _collection.Count.Should().Be(0, "Should be 1 item in the collection"); + } + [Fact] public void SmallChangeDoesNotInvokeReset() { @@ -121,61 +112,73 @@ public void SmallChangeDoesNotInvokeReset() bool invoked = false; bool resetInvoked = false; _collection.ListChanged += (sender, e) => - { - invoked = true; - if (e.ListChangedType == ListChangedType.Reset) { - resetInvoked = true; - } - }; + invoked = true; + if (e.ListChangedType == ListChangedType.Reset) + { + resetInvoked = true; + } + }; _source.AddOrUpdate(_generator.Take(24)); invoked.Should().BeTrue(); resetInvoked.Should().BeFalse(); } - [Fact] - public void TreatMovesAsRemoveAdd() - { - var cache = new SourceCache(p => p.Name); - - var people = Enumerable.Range(0,10).Select(age => new Person("Person" + age, age)).ToList(); - var importantGuy = people.First(); - cache.AddOrUpdate(people); - - ISortedChangeSet latestSetWithoutMoves = null; - ISortedChangeSet latestSetWithMoves = null; - - var boundList1 = new ObservableCollectionExtended(); - var boundList2 = new ObservableCollectionExtended(); - - using (cache.Connect() - .AutoRefresh(p => p.Age) - .Sort(SortExpressionComparer.Ascending(p => p.Age)) - .TreatMovesAsRemoveAdd() - .Bind(boundList1) - .Subscribe(set => latestSetWithoutMoves = set)) - - using (cache.Connect() - .AutoRefresh(p => p.Age) - .Sort(SortExpressionComparer.Ascending(p => p.Age)) - .Bind(boundList2) - .Subscribe(set => latestSetWithMoves = set)) - { - - importantGuy.Age = importantGuy.Age + 200; - - latestSetWithoutMoves.Removes.Should().Be(1); - latestSetWithoutMoves.Adds.Should().Be(1); - latestSetWithoutMoves.Moves.Should().Be(0); - latestSetWithoutMoves.Updates.Should().Be(0); - - latestSetWithMoves.Moves.Should().Be(1); - latestSetWithMoves.Updates.Should().Be(0); - latestSetWithMoves.Removes.Should().Be(0); - latestSetWithMoves.Adds.Should().Be(0); - } - } + [Fact] + public void TreatMovesAsRemoveAdd() + { + var cache = new SourceCache(p => p.Name); + + var people = Enumerable.Range(0, 10).Select(age => new Person("Person" + age, age)).ToList(); + var importantGuy = people.First(); + cache.AddOrUpdate(people); + + ISortedChangeSet? latestSetWithoutMoves = null; + ISortedChangeSet? latestSetWithMoves = null; + + var boundList1 = new ObservableCollectionExtended(); + var boundList2 = new ObservableCollectionExtended(); + + using (cache.Connect().AutoRefresh(p => p.Age).Sort(SortExpressionComparer.Ascending(p => p.Age)).TreatMovesAsRemoveAdd().Bind(boundList1).Subscribe(set => latestSetWithoutMoves = set)) + + using (cache.Connect().AutoRefresh(p => p.Age).Sort(SortExpressionComparer.Ascending(p => p.Age)).Bind(boundList2).Subscribe(set => latestSetWithMoves = set)) + { + if (latestSetWithoutMoves is null) + { + throw new InvalidOperationException(nameof(latestSetWithoutMoves)); + } + + if (latestSetWithMoves is null) + { + throw new InvalidOperationException(nameof(latestSetWithMoves)); + } + + importantGuy.Age += 200; + latestSetWithoutMoves.Should().NotBeNull(); + latestSetWithoutMoves.Removes.Should().Be(1); + latestSetWithoutMoves.Adds.Should().Be(1); + latestSetWithoutMoves.Moves.Should().Be(0); + latestSetWithoutMoves.Updates.Should().Be(0); + + latestSetWithMoves.Moves.Should().Be(1); + latestSetWithMoves.Updates.Should().Be(0); + latestSetWithMoves.Removes.Should().Be(0); + latestSetWithMoves.Adds.Should().Be(0); + } + } + + [Fact] + public void UpdateToSourceUpdatesTheDestination() + { + var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); + _source.AddOrUpdate(person); + _source.AddOrUpdate(personUpdated); + + _collection.Count.Should().Be(1, "Should be 1 item in the collection"); + _collection.First().Should().Be(personUpdated, "Should be updated person"); + } } } #endif \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/BindingListToChangeSetFixture.cs b/src/DynamicData.Tests/Binding/BindingListToChangeSetFixture.cs index 1e759ab8f..fb646be3f 100644 --- a/src/DynamicData.Tests/Binding/BindingListToChangeSetFixture.cs +++ b/src/DynamicData.Tests/Binding/BindingListToChangeSetFixture.cs @@ -1,8 +1,11 @@ using System; using System.ComponentModel; using System.Linq; + using DynamicData.Binding; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding @@ -10,6 +13,7 @@ namespace DynamicData.Tests.Binding public class BindingListToChangeSetFixture : IDisposable { private readonly TestBindingList _collection; + private readonly ChangeSetAggregator _results; public BindingListToChangeSetFixture() @@ -18,11 +22,6 @@ public BindingListToChangeSetFixture() _results = _collection.ToObservableChangeSet().AsAggregator(); } - public void Dispose() - { - _results.Dispose(); - } - [Fact] public void Add() { @@ -33,16 +32,9 @@ public void Add() _results.Data.Items.First().Should().Be(1); } - [Fact] - public void Remove() + public void Dispose() { - _collection.AddRange(Enumerable.Range(1, 10)); - - _collection.Remove(3); - - _results.Data.Count.Should().Be(9); - _results.Data.Items.Contains(3).Should().BeFalse(); - _results.Data.Items.Should().BeEquivalentTo(_collection); + _results.Dispose(); } [Fact] @@ -54,28 +46,6 @@ public void Duplicates() _results.Data.Count.Should().Be(2); } - [Fact] - public void Replace() - { - _collection.AddRange(Enumerable.Range(1, 10)); - _collection[8] = 20; - - _results.Data.Items.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 20, 10 }); - } - - [Fact] - public void ResetFiresClearsAndAdds() - { - _collection.AddRange(Enumerable.Range(1, 10)); - - _collection.Reset(); - _results.Data.Items.Should().BeEquivalentTo(_collection); - - var resetNotification = _results.Messages.Last(); - resetNotification.Removes.Should().Be(10); - resetNotification.Adds.Should().Be(10); - } - [Fact] public void RaiseListChangedEvents() { @@ -96,17 +66,13 @@ public void RefreshCausesReplace() // Arrange var sourceCache = new SourceCache(item => item.Id); - var item1 = new Item(name: "Old Name"); + var item1 = new Item("Old Name"); sourceCache.AddOrUpdate(item1); var collection = new TestBindingList(); - var sourceCacheResults = sourceCache - .Connect() - .AutoRefresh(item => item.Name) - .Bind(collection) - .AsAggregator(); + var sourceCacheResults = sourceCache.Connect().AutoRefresh(item => item.Name).Bind(collection).AsAggregator(); var collectionResults = collection.ToObservableChangeSet().AsAggregator(); @@ -129,6 +95,40 @@ public void RefreshCausesReplace() collectionResults.Dispose(); } + [Fact] + public void Remove() + { + _collection.AddRange(Enumerable.Range(1, 10)); + + _collection.Remove(3); + + _results.Data.Count.Should().Be(9); + _results.Data.Items.Contains(3).Should().BeFalse(); + _results.Data.Items.Should().BeEquivalentTo(_collection); + } + + [Fact] + public void Replace() + { + _collection.AddRange(Enumerable.Range(1, 10)); + _collection[8] = 20; + + _results.Data.Items.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 20, 10 }); + } + + [Fact] + public void ResetFiresClearsAndAdds() + { + _collection.AddRange(Enumerable.Range(1, 10)); + + _collection.Reset(); + _results.Data.Items.Should().BeEquivalentTo(_collection); + + var resetNotification = _results.Messages.Last(); + resetNotification.Removes.Should().Be(10); + resetNotification.Adds.Should().Be(10); + } + private class TestBindingList : BindingList { public void Reset() @@ -137,4 +137,4 @@ public void Reset() } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/DeeplyNestedNotifyPropertyChangedFixture.cs b/src/DynamicData.Tests/Binding/DeeplyNestedNotifyPropertyChangedFixture.cs index 0e324829b..ae7d33860 100644 --- a/src/DynamicData.Tests/Binding/DeeplyNestedNotifyPropertyChangedFixture.cs +++ b/src/DynamicData.Tests/Binding/DeeplyNestedNotifyPropertyChangedFixture.cs @@ -2,22 +2,46 @@ using System.Diagnostics; using System.Linq; using System.Reactive.Linq; + using DynamicData.Binding; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - public class DeeplyNestedNotifyPropertyChangedFixture { + [Fact] + public void DepthOfOne() + { + var instance = new ClassA { Name = "Someone" }; + + var chain = instance.WhenPropertyChanged(a => a.Name, true); + string? result = null; + + var subscription = chain.Subscribe(notification => result = notification?.Value); + + result.Should().Be("Someone"); + + instance.Name = "Else"; + result.Should().Be("Else"); + + instance.Name = null; + result.Should().Be(null); + + instance.Name = "NotNull"; + result.Should().Be("NotNull"); + } + [Fact] public void NotifiesInitialValue_WithFallback() { - var instance = new ClassA {Child = new ClassB {Age = 10}}; + var instance = new ClassA { Child = new ClassB { Age = 10 } }; //provide a fallback so a value can always be obtained - var chain = instance.WhenChanged(a => a.Child.Age, (sender, a) => a, () => -1); + var chain = instance.WhenChanged(a => a!.Child!.Age, (sender, a) => a, () => -1); int? result = null; @@ -28,7 +52,7 @@ public void NotifiesInitialValue_WithFallback() instance.Child.Age = 22; result.Should().Be(22); - instance.Child = new ClassB {Age = 25}; + instance.Child = new ClassB { Age = 25 }; result.Should().Be(25); instance.Child.Age = 26; @@ -45,7 +69,7 @@ public void NotifiesInitialValueAndNullChild() { var instance = new ClassA(); - var chain = instance.WhenPropertyChanged(a => a.Child.Age, true); + var chain = instance.WhenPropertyChanged(a => a.Child!.Age, true); int? result = null; var subscription = chain.Subscribe(notification => result = notification?.Value); @@ -63,26 +87,29 @@ public void NotifiesInitialValueAndNullChild() instance.Child.Age = 26; result.Should().Be(26); instance.Child = null; - } [Fact] - public void WithoutInitialValue() + public void NullChildWithInitialValue() { - var instance = new ClassA {Name="TestClass", Child = new ClassB {Age = 10}}; + var instance = new ClassA(); - var chain = instance.WhenPropertyChanged(a => a.Child.Age, false); + var chain = instance.WhenPropertyChanged(a => a!.Child!.Age, true); int? result = null; - var subscription = chain.Subscribe(notification => result = notification.Value); + var subscription = chain.Subscribe(notification => result = notification?.Value); result.Should().Be(null); + instance.Child = new ClassB { Age = 21 }; + result.Should().Be(21); + instance.Child.Age = 22; result.Should().Be(22); - instance.Child = new ClassB {Age = 25}; + instance.Child = new ClassB { Age = 25 }; result.Should().Be(25); + instance.Child.Age = 30; result.Should().Be(30); } @@ -92,7 +119,7 @@ public void NullChildWithoutInitialValue() { var instance = new ClassA(); - var chain = instance.WhenPropertyChanged(a => a.Child.Age, false); + var chain = instance.WhenPropertyChanged(a => a!.Child!.Age, false); int? result = null; var subscription = chain.Subscribe(notification => result = notification.Value); @@ -113,99 +140,92 @@ public void NullChildWithoutInitialValue() } [Fact] - public void NullChildWithInitialValue() + public void WithoutInitialValue() { - var instance = new ClassA(); + var instance = new ClassA { Name = "TestClass", Child = new ClassB { Age = 10 } }; - var chain = instance.WhenPropertyChanged(a => a.Child.Age, true); + var chain = instance.WhenPropertyChanged(a => a!.Child!.Age, false); int? result = null; - var subscription = chain.Subscribe(notification => result = notification?.Value); + var subscription = chain.Subscribe(notification => result = notification.Value); result.Should().Be(null); - instance.Child = new ClassB { Age = 21 }; - result.Should().Be(21); - instance.Child.Age = 22; result.Should().Be(22); instance.Child = new ClassB { Age = 25 }; result.Should().Be(25); - instance.Child.Age = 30; result.Should().Be(30); } - [Fact] - public void DepthOfOne() - { - var instance = new ClassA {Name="Someone"}; - - var chain = instance.WhenPropertyChanged(a => a.Name, true); - string result = null; - - var subscription = chain.Subscribe(notification => result = notification?.Value); - - result.Should().Be("Someone"); - - instance.Name = "Else"; - result.Should().Be("Else"); - - instance.Name = null; - result.Should().Be(null); - - instance.Name = "NotNull"; - result.Should().Be("NotNull"); - - } - - // [Fact] - // [Trait("Manual run for benchmarking","xx")] + // [Fact] + // [Trait("Manual run for benchmarking","xx")] private void StressIt() { var list = new SourceList(); - var items = Enumerable.Range(1, 10_000) - .Select(i => new ClassA { Name = i.ToString(), Child = new ClassB { Age = i } }) - .ToArray(); + var items = Enumerable.Range(1, 10_000).Select(i => new ClassA { Name = i.ToString(), Child = new ClassB { Age = i } }).ToArray(); list.AddRange(items); var sw = new Stopwatch(); - // var factory = + // var factory = - var myObservable = list.Connect() - .Do(_ => sw.Start()) - .WhenPropertyChanged(a => a.Child.Age, false) - .Do(_ => sw.Stop()) - .Subscribe(); + var myObservable = list.Connect().Do(_ => sw.Start()).WhenPropertyChanged(a => a!.Child!.Age, false).Do(_ => sw.Stop()).Subscribe(); + + if (items.Length > 1 && items[1].Child is not null) + { +#pragma warning disable CS8602 // Dereference of a possibly null reference. + items[1].Child.Age = -1; +#pragma warning restore CS8602 // Dereference of a possibly null reference. + } + else + { + throw new InvalidOperationException(nameof(items)); + } - items[1].Child.Age=-1; Console.WriteLine($"{sw.ElapsedMilliseconds}"); } - public class ClassA: AbstractNotifyPropertyChanged, IEquatable + public class ClassA : AbstractNotifyPropertyChanged, IEquatable { - private string _name; + private ClassB? _classB; + + private string? _name; + + public ClassB? Child + { + get => _classB; + set => SetAndRaise(ref _classB, value); + } - public string Name + public string? Name { get => _name; set => SetAndRaise(ref _name, value); } - private ClassB _classB; - - public ClassB Child + /// Returns a value that indicates whether the values of two objects are equal. + /// The first value to compare. + /// The second value to compare. + /// true if the and parameters have the same value; otherwise, false. + public static bool operator ==(ClassA left, ClassA right) { - get => _classB; - set => SetAndRaise(ref _classB, value); + return Equals(left, right); } - #region Equality + /// Returns a value that indicates whether two objects have different values. + /// The first value to compare. + /// The second value to compare. + /// true if and are not equal; otherwise, false. + public static bool operator !=(ClassA left, ClassA right) + { + return !Equals(left, right); + } - public bool Equals(ClassA other) + public bool Equals(ClassA? other) { if (ReferenceEquals(null, other)) { @@ -220,7 +240,7 @@ public bool Equals(ClassA other) return string.Equals(_name, other._name) && Equals(_classB, other._classB); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -237,37 +257,17 @@ public override bool Equals(object obj) return false; } - return Equals((ClassA) obj); + return Equals((ClassA)obj); } public override int GetHashCode() { unchecked { - return ((_name != null ? _name.GetHashCode() : 0) * 397) ^ (_classB != null ? _classB.GetHashCode() : 0); + return ((_name is not null ? _name.GetHashCode() : 0) * 397) ^ (_classB is not null ? _classB.GetHashCode() : 0); } } - /// Returns a value that indicates whether the values of two objects are equal. - /// The first value to compare. - /// The second value to compare. - /// true if the and parameters have the same value; otherwise, false. - public static bool operator ==(ClassA left, ClassA right) - { - return Equals(left, right); - } - - /// Returns a value that indicates whether two objects have different values. - /// The first value to compare. - /// The second value to compare. - /// true if and are not equal; otherwise, false. - public static bool operator !=(ClassA left, ClassA right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"ClassA: Name={Name}, {nameof(Child)}: {Child}"; @@ -284,9 +284,25 @@ public int Age set => SetAndRaise(ref _age, value); } - #region Equality + /// Returns a value that indicates whether the values of two objects are equal. + /// The first value to compare. + /// The second value to compare. + /// true if the and parameters have the same value; otherwise, false. + public static bool operator ==(ClassB left, ClassB right) + { + return Equals(left, right); + } - public bool Equals(ClassB other) + /// Returns a value that indicates whether two objects have different values. + /// The first value to compare. + /// The second value to compare. + /// true if and are not equal; otherwise, false. + public static bool operator !=(ClassB left, ClassB right) + { + return !Equals(left, right); + } + + public bool Equals(ClassB? other) { if (ReferenceEquals(null, other)) { @@ -301,7 +317,7 @@ public bool Equals(ClassB other) return _age == other._age; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -318,7 +334,7 @@ public override bool Equals(object obj) return false; } - return Equals((ClassB) obj); + return Equals((ClassB)obj); } public override int GetHashCode() @@ -326,31 +342,10 @@ public override int GetHashCode() return _age; } - /// Returns a value that indicates whether the values of two objects are equal. - /// The first value to compare. - /// The second value to compare. - /// true if the and parameters have the same value; otherwise, false. - public static bool operator ==(ClassB left, ClassB right) - { - return Equals(left, right); - } - - /// Returns a value that indicates whether two objects have different values. - /// The first value to compare. - /// The second value to compare. - /// true if and are not equal; otherwise, false. - public static bool operator !=(ClassB left, ClassB right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{nameof(Age)}: {Age}"; } } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/IObservableListBindCacheFixture.cs b/src/DynamicData.Tests/Binding/IObservableListBindCacheFixture.cs index f705a3ab2..4f00124f9 100644 --- a/src/DynamicData.Tests/Binding/IObservableListBindCacheFixture.cs +++ b/src/DynamicData.Tests/Binding/IObservableListBindCacheFixture.cs @@ -1,40 +1,35 @@ using System; using System.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - - public class IObservableListBindCacheFixture: IDisposable + public class IObservableListBindCacheFixture : IDisposable { + private readonly RandomPersonGenerator _generator = new(); + private readonly IObservableList _list; + private readonly ChangeSetAggregator _listNotifications; + private readonly ISourceCache _source; + private readonly ChangeSetAggregator _sourceCacheNotifications; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); public IObservableListBindCacheFixture() { _source = new SourceCache(p => p.Name); - _sourceCacheNotifications = _source - .Connect() - .AutoRefresh() - .BindToObservableList(out _list) - .AsAggregator(); + _sourceCacheNotifications = _source.Connect().AutoRefresh().BindToObservableList(out _list).AsAggregator(); _listNotifications = _list.Connect().AsAggregator(); } - public void Dispose() - { - _sourceCacheNotifications.Dispose(); - _listNotifications.Dispose(); - _source.Dispose(); - } - [Fact] public void AddToSourceAddsToDestination() { @@ -45,28 +40,6 @@ public void AddToSourceAddsToDestination() _list.Items.First().Should().Be(person, "Should be same person"); } - [Fact] - public void UpdateToSourceUpdatesTheDestination() - { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.AddOrUpdate(person); - _source.AddOrUpdate(personUpdated); - - _list.Count.Should().Be(1, "Should be 1 item in the collection"); - _list.Items.First().Should().Be(personUpdated, "Should be updated person"); - } - - [Fact] - public void RemoveSourceRemovesFromTheDestination() - { - var person = new Person("Adult1", 50); - _source.AddOrUpdate(person); - _source.Remove(person); - - _list.Count.Should().Be(0, "Should be 1 item in the collection"); - } - [Fact] public void BatchAdd() { @@ -86,6 +59,13 @@ public void BatchRemove() _list.Count.Should().Be(0, "Should be 100 items in the collection"); } + public void Dispose() + { + _sourceCacheNotifications.Dispose(); + _listNotifications.Dispose(); + _source.Dispose(); + } + [Fact] public void ListRecievesRefresh() { @@ -97,5 +77,27 @@ public void ListRecievesRefresh() _listNotifications.Messages.Count().Should().Be(2); _listNotifications.Messages.Last().First().Reason.Should().Be(ListChangeReason.Refresh); } + + [Fact] + public void RemoveSourceRemovesFromTheDestination() + { + var person = new Person("Adult1", 50); + _source.AddOrUpdate(person); + _source.Remove(person); + + _list.Count.Should().Be(0, "Should be 1 item in the collection"); + } + + [Fact] + public void UpdateToSourceUpdatesTheDestination() + { + var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); + _source.AddOrUpdate(person); + _source.AddOrUpdate(personUpdated); + + _list.Count.Should().Be(1, "Should be 1 item in the collection"); + _list.Items.First().Should().Be(personUpdated, "Should be updated person"); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/IObservableListBindCacheSortedFixture.cs b/src/DynamicData.Tests/Binding/IObservableListBindCacheSortedFixture.cs index 2963887db..92f7f4fd5 100644 --- a/src/DynamicData.Tests/Binding/IObservableListBindCacheSortedFixture.cs +++ b/src/DynamicData.Tests/Binding/IObservableListBindCacheSortedFixture.cs @@ -1,41 +1,82 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - public class IObservableListBindCacheSortedFixture : IDisposable { private static readonly IComparer _comparerAgeAscThanNameAsc = SortExpressionComparer.Ascending(p => p.Age).ThenByAscending(p => p.Name); + private static readonly IComparer _comparerNameDesc = SortExpressionComparer.Descending(p => p.Name); + private readonly BehaviorSubject> _comparer = new BehaviorSubject>(_comparerAgeAscThanNameAsc); + + private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + private readonly IObservableList _list; + private readonly ChangeSetAggregator _listNotifications; + private readonly ISourceCache _source; + private readonly SortedChangeSetAggregator _sourceCacheNotifications; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); - private readonly BehaviorSubject> _comparer = new BehaviorSubject>(_comparerAgeAscThanNameAsc); public IObservableListBindCacheSortedFixture() { _source = new SourceCache(p => p.Name); - _sourceCacheNotifications = _source - .Connect() - .AutoRefresh() - .Sort(_comparer, resetThreshold: 10) - .BindToObservableList(out _list) - .AsAggregator(); + _sourceCacheNotifications = _source.Connect().AutoRefresh().Sort(_comparer, resetThreshold: 10).BindToObservableList(out _list).AsAggregator(); _listNotifications = _list.Connect().AsAggregator(); } + [Fact] + public void AddToSourceAddsToDestination() + { + var person = new Person("Adult1", 50); + _source.AddOrUpdate(person); + + _list.Count.Should().Be(1, "Should be 1 item in the collection"); + _list.Items.First().Should().Be(person, "Should be same person"); + } + + [Fact] + public void BatchAdd() + { + var people = _generator.Take(15).ToList(); + _source.AddOrUpdate(people); + + var sorted = people.OrderBy(p => p, _comparerAgeAscThanNameAsc).ToList(); + + _list.Count.Should().Be(15, "Should be 15 items in the collection"); + _list.Items.Should().Equal(sorted, "Collections should be equivalent"); + } + + [Fact] + public void BatchRemove() + { + var people = _generator.Take(100).ToList(); + _source.AddOrUpdate(people); + _source.Clear(); + _list.Count.Should().Be(0, "Should be 0 items in the collection"); + } + + [Fact] + public void CollectionIsInSortOrder() + { + _source.AddOrUpdate(_generator.Take(100)); + var sorted = _source.Items.OrderBy(p => p, _comparerAgeAscThanNameAsc).ToList(); + sorted.Should().Equal(_list.Items); + } + public void Dispose() { _sourceCacheNotifications.Dispose(); @@ -54,19 +95,14 @@ public void InitialBindWithExistingData() source.AddOrUpdate(person2); // Add out of order to assert intial order source.AddOrUpdate(person1); - var sourceCacheNotifications = source - .Connect() - .AutoRefresh() - .Sort(_comparer, resetThreshold: 10) - .BindToObservableList(out var list) - .AsAggregator(); + var sourceCacheNotifications = source.Connect().AutoRefresh().Sort(_comparer, resetThreshold: 10).BindToObservableList(out var list).AsAggregator(); var listNotifications = list.Connect().AsAggregator(); // Assert listNotifications.Messages.Count().Should().Be(1); listNotifications.Messages.First().First().Reason.Should().Be(ListChangeReason.AddRange); - list.Items.Should().Equal(new Person[] { person1, person2 }); + list.Items.Should().Equal(person1, person2); // Clean up source.Dispose(); @@ -76,69 +112,60 @@ public void InitialBindWithExistingData() } [Fact] - public void AddToSourceAddsToDestination() + public void ListRecievesMoves() { - var person = new Person("Adult1", 50); - _source.AddOrUpdate(person); + var person1 = new Person("Person1", 10); + var person2 = new Person("Person2", 20); + var person3 = new Person("Person3", 30); - _list.Count.Should().Be(1, "Should be 1 item in the collection"); - _list.Items.First().Should().Be(person, "Should be same person"); - } + _source.AddOrUpdate(new Person[] { person1, person2, person3 }); - [Fact] - public void UpdateToSourceUpdatesTheDestination() - { - var person1 = new Person("Adult1", 20); - var person2 = new Person("Adult2", 30); - var personUpdated1 = new Person("Adult1", 40); + // Move person 3 to the front on the line + person3.Age = 1; - _source.AddOrUpdate(person1); - _source.AddOrUpdate(person2); + // 1 ChangeSet with AddRange & 1 ChangeSet with Refresh & Move + _listNotifications.Messages.Count().Should().Be(2); - _list.Items.Should().Equal(new Person[] { person1, person2 }); + // Assert AddRange + var addChangeSet = _listNotifications.Messages.First(); + addChangeSet.First().Reason.Should().Be(ListChangeReason.AddRange); - _source.AddOrUpdate(personUpdated1); + // Assert Refresh & Move + var refreshAndMoveChangeSet = _listNotifications.Messages.Last(); - _list.Items.Should().Equal(new Person[] { person2, personUpdated1 }); - } + refreshAndMoveChangeSet.Count.Should().Be(2); - [Fact] - public void RemoveSourceRemovesFromTheDestination() - { - var person = new Person("Adult1", 50); - _source.AddOrUpdate(person); - _source.Remove(person); + var refreshChange = refreshAndMoveChangeSet.First(); + refreshChange.Reason.Should().Be(ListChangeReason.Refresh); + refreshChange.Item.Current.Should().Be(person3); - _list.Count.Should().Be(0, "Should be 1 item in the collection"); + var moveChange = refreshAndMoveChangeSet.Last(); + moveChange.Reason.Should().Be(ListChangeReason.Moved); + moveChange.Item.Current.Should().Be(person3); + moveChange.Item.PreviousIndex.Should().Be(2); + moveChange.Item.CurrentIndex.Should().Be(0); } [Fact] - public void BatchAdd() + public void ListRecievesRefresh() { - var people = _generator.Take(15).ToList(); - _source.AddOrUpdate(people); + var person = new Person("Adult1", 50); + _source.AddOrUpdate(person); - var sorted = people.OrderBy(p => p, _comparerAgeAscThanNameAsc).ToList(); + person.Age = 60; - _list.Count.Should().Be(15, "Should be 15 items in the collection"); - _list.Items.Should().Equal(sorted, "Collections should be equivalent"); + _listNotifications.Messages.Count().Should().Be(2); + _listNotifications.Messages.Last().First().Reason.Should().Be(ListChangeReason.Refresh); } [Fact] - public void BatchRemove() + public void RemoveSourceRemovesFromTheDestination() { - var people = _generator.Take(100).ToList(); - _source.AddOrUpdate(people); - _source.Clear(); - _list.Count.Should().Be(0, "Should be 0 items in the collection"); - } + var person = new Person("Adult1", 50); + _source.AddOrUpdate(person); + _source.Remove(person); - [Fact] - public void CollectionIsInSortOrder() - { - _source.AddOrUpdate(_generator.Take(100)); - var sorted = _source.Items.OrderBy(p => p, _comparerAgeAscThanNameAsc).ToList(); - sorted.Should().Equal(_list.Items); + _list.Count.Should().Be(0, "Should be 1 item in the collection"); } [Fact] @@ -155,97 +182,67 @@ public void Reset() _list.Items.Should().Equal(sorted); _listNotifications.Messages.Count().Should().Be(2); // Initial loading change set and a reset change due to a change over the reset threshold. - _listNotifications.Messages[0].First().Reason.Should().Be(ListChangeReason.AddRange);// initial loading - _listNotifications.Messages[1].Count.Should().Be(2);// Reset + _listNotifications.Messages[0].First().Reason.Should().Be(ListChangeReason.AddRange); // initial loading + _listNotifications.Messages[1].Count.Should().Be(2); // Reset _listNotifications.Messages[1].First().Reason.Should().Be(ListChangeReason.Clear); // reset _listNotifications.Messages[1].Last().Reason.Should().Be(ListChangeReason.AddRange); // reset } [Fact] - public void ListRecievesRefresh() + public void TreatMovesAsRemoveAdd() { - var person = new Person("Adult1", 50); - _source.AddOrUpdate(person); + var cache = new SourceCache(p => p.Name); - person.Age = 60; + var people = Enumerable.Range(0, 10).Select(age => new Person("Person" + age, age)).ToList(); + var importantGuy = people.First(); + cache.AddOrUpdate(people); - _listNotifications.Messages.Count().Should().Be(2); - _listNotifications.Messages.Last().First().Reason.Should().Be(ListChangeReason.Refresh); - } + ISortedChangeSet? latestSetWithoutMoves = null; + ISortedChangeSet? latestSetWithMoves = null; - [Fact] - public void ListRecievesMoves() - { - var person1 = new Person("Person1", 10); - var person2 = new Person("Person2", 20); - var person3 = new Person("Person3", 30); + using (cache.Connect().AutoRefresh(p => p.Age).Sort(SortExpressionComparer.Ascending(p => p.Age)).TreatMovesAsRemoveAdd().BindToObservableList(out var boundList1).Subscribe(set => latestSetWithoutMoves = set)) - _source.AddOrUpdate(new Person[] { person1, person2, person3 }); + using (cache.Connect().AutoRefresh(p => p.Age).Sort(SortExpressionComparer.Ascending(p => p.Age)).BindToObservableList(out var boundList2).Subscribe(set => latestSetWithMoves = set)) + { + importantGuy.Age += 200; - // Move person 3 to the front on the line - person3.Age = 1; + if (latestSetWithoutMoves is null) + { + throw new InvalidOperationException(nameof(latestSetWithoutMoves)); + } - // 1 ChangeSet with AddRange & 1 ChangeSet with Refresh & Move - _listNotifications.Messages.Count().Should().Be(2); + if (latestSetWithMoves is null) + { + throw new InvalidOperationException(nameof(latestSetWithMoves)); + } - // Assert AddRange - var addChangeSet = _listNotifications.Messages.First(); - addChangeSet.First().Reason.Should().Be(ListChangeReason.AddRange); + latestSetWithoutMoves.Removes.Should().Be(1); + latestSetWithoutMoves.Adds.Should().Be(1); + latestSetWithoutMoves.Moves.Should().Be(0); + latestSetWithoutMoves.Updates.Should().Be(0); - // Assert Refresh & Move - var refreshAndMoveChangeSet = _listNotifications.Messages.Last(); + latestSetWithMoves.Moves.Should().Be(1); + latestSetWithMoves.Updates.Should().Be(0); + latestSetWithMoves.Removes.Should().Be(0); + latestSetWithMoves.Adds.Should().Be(0); + } + } - refreshAndMoveChangeSet.Count.Should().Be(2); + [Fact] + public void UpdateToSourceUpdatesTheDestination() + { + var person1 = new Person("Adult1", 20); + var person2 = new Person("Adult2", 30); + var personUpdated1 = new Person("Adult1", 40); - var refreshChange = refreshAndMoveChangeSet.First(); - refreshChange.Reason.Should().Be(ListChangeReason.Refresh); - refreshChange.Item.Current.Should().Be(person3); + _source.AddOrUpdate(person1); + _source.AddOrUpdate(person2); - var moveChange = refreshAndMoveChangeSet.Last(); - moveChange.Reason.Should().Be(ListChangeReason.Moved); - moveChange.Item.Current.Should().Be(person3); - moveChange.Item.PreviousIndex.Should().Be(2); - moveChange.Item.CurrentIndex.Should().Be(0); - } + _list.Items.Should().Equal(person1, person2); - [Fact] - public void TreatMovesAsRemoveAdd() - { - var cache = new SourceCache(p => p.Name); - - var people = Enumerable.Range(0,10).Select(age => new Person("Person" + age, age)).ToList(); - var importantGuy = people.First(); - cache.AddOrUpdate(people); - - ISortedChangeSet latestSetWithoutMoves = null; - ISortedChangeSet latestSetWithMoves = null; - - using (cache.Connect() - .AutoRefresh(p => p.Age) - .Sort(SortExpressionComparer.Ascending(p => p.Age)) - .TreatMovesAsRemoveAdd() - .BindToObservableList(out var boundList1) - .Subscribe(set => latestSetWithoutMoves = set)) - - using (cache.Connect() - .AutoRefresh(p => p.Age) - .Sort(SortExpressionComparer.Ascending(p => p.Age)) - .BindToObservableList(out var boundList2) - .Subscribe(set => latestSetWithMoves = set)) - { - - importantGuy.Age = importantGuy.Age + 200; - - latestSetWithoutMoves.Removes.Should().Be(1); - latestSetWithoutMoves.Adds.Should().Be(1); - latestSetWithoutMoves.Moves.Should().Be(0); - latestSetWithoutMoves.Updates.Should().Be(0); - - latestSetWithMoves.Moves.Should().Be(1); - latestSetWithMoves.Updates.Should().Be(0); - latestSetWithMoves.Removes.Should().Be(0); - latestSetWithMoves.Adds.Should().Be(0); - } - } + _source.AddOrUpdate(personUpdated1); + + _list.Items.Should().Equal(person2, personUpdated1); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/IObservableListBindListFixture.cs b/src/DynamicData.Tests/Binding/IObservableListBindListFixture.cs index 36fcf98cb..35b8ad3b7 100644 --- a/src/DynamicData.Tests/Binding/IObservableListBindListFixture.cs +++ b/src/DynamicData.Tests/Binding/IObservableListBindListFixture.cs @@ -1,38 +1,43 @@ using System; using System.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - - public class IObservableListBindListFixture: IDisposable + public class IObservableListBindListFixture : IDisposable { + private readonly RandomPersonGenerator _generator = new(); + private readonly IObservableList _list; + private readonly ChangeSetAggregator _observableListNotifications; + private readonly SourceList _source; + private readonly ChangeSetAggregator _sourceListNotifications; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); public IObservableListBindListFixture() { _source = new SourceList(); - _sourceListNotifications = _source - .Connect() - .AutoRefresh() - .BindToObservableList(out _list) - .AsAggregator(); + _sourceListNotifications = _source.Connect().AutoRefresh().BindToObservableList(out _list).AsAggregator(); _observableListNotifications = _list.Connect().AsAggregator(); } - public void Dispose() + [Fact] + public void AddRange() { - _sourceListNotifications.Dispose(); - _observableListNotifications.Dispose(); - _source.Dispose(); + var people = _generator.Take(100).ToList(); + _source.AddRange(people); + + _list.Count.Should().Be(100, "Should be 100 items in the collection"); + _list.Should().BeEquivalentTo(_list, "Collections should be equivalent"); } [Fact] @@ -46,56 +51,53 @@ public void AddToSourceAddsToDestination() } [Fact] - public void UpdateToSourceUpdatesTheDestination() + public void Clear() { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.Add(person); - _source.Replace(person, personUpdated); + var people = _generator.Take(100).ToList(); + _source.AddRange(people); + _source.Clear(); + _list.Count.Should().Be(0, "Should be 100 items in the collection"); + } - _list.Count.Should().Be(1, "Should be 1 item in the collection"); - _list.Items.First().Should().Be(personUpdated, "Should be updated person"); + public void Dispose() + { + _sourceListNotifications.Dispose(); + _observableListNotifications.Dispose(); + _source.Dispose(); } [Fact] - public void RemoveSourceRemovesFromTheDestination() + public void ListRecievesRefresh() { var person = new Person("Adult1", 50); _source.Add(person); - _source.Remove(person); - - _list.Count.Should().Be(0, "Should be 1 item in the collection"); - } - [Fact] - public void AddRange() - { - var people = _generator.Take(100).ToList(); - _source.AddRange(people); + person.Age = 60; - _list.Count.Should().Be(100, "Should be 100 items in the collection"); - _list.Should().BeEquivalentTo(_list, "Collections should be equivalent"); + _observableListNotifications.Messages.Count().Should().Be(2); + _observableListNotifications.Messages.Last().First().Reason.Should().Be(ListChangeReason.Refresh); } [Fact] - public void Clear() + public void RemoveSourceRemovesFromTheDestination() { - var people = _generator.Take(100).ToList(); - _source.AddRange(people); - _source.Clear(); - _list.Count.Should().Be(0, "Should be 100 items in the collection"); + var person = new Person("Adult1", 50); + _source.Add(person); + _source.Remove(person); + + _list.Count.Should().Be(0, "Should be 1 item in the collection"); } [Fact] - public void ListRecievesRefresh() + public void UpdateToSourceUpdatesTheDestination() { var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); _source.Add(person); + _source.Replace(person, personUpdated); - person.Age = 60; - - _observableListNotifications.Messages.Count().Should().Be(2); - _observableListNotifications.Messages.Last().First().Reason.Should().Be(ListChangeReason.Refresh); + _list.Count.Should().Be(1, "Should be 1 item in the collection"); + _list.Items.First().Should().Be(personUpdated, "Should be updated person"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/NotifyPropertyChangedExFixture.cs b/src/DynamicData.Tests/Binding/NotifyPropertyChangedExFixture.cs index 532e24a18..8a2620d5a 100644 --- a/src/DynamicData.Tests/Binding/NotifyPropertyChangedExFixture.cs +++ b/src/DynamicData.Tests/Binding/NotifyPropertyChangedExFixture.cs @@ -1,55 +1,20 @@ using System; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { public class NotifyPropertyChangedExFixture { - [Theory, - InlineData(true), - InlineData(false)] - public void SubscribeToValueChangeForAllItemsInList( bool notifyOnInitialValue) - { - var lastAgeChange = -1; - var source = new SourceList(); - source.Connect().WhenValueChanged(p => p.Age, notifyOnInitialValue).Subscribe(i => lastAgeChange = i); - var person = new Person("Name", 10); - var anotherPerson = new Person("AnotherName", 10); - source.Add(person); - source.Add(anotherPerson); - - (notifyOnInitialValue ? 10 : -1).Should().Be(lastAgeChange); - person.Age = 12; - 12.Should().Be(lastAgeChange); - anotherPerson.Age = 13; - 13.Should().Be(lastAgeChange); - } - - [Theory, - InlineData(true), - InlineData(false)] - public void SubscribeToValueChangedOnASingleItem( bool notifyOnInitialValue) - { - var age = -1; - var person = new Person("Name", 10); - person.WhenValueChanged(p => p.Age, notifyOnInitialValue).Subscribe(i => age = i); - - (notifyOnInitialValue ? 10 : -1).Should().Be(age); - person.Age = 12; - 12.Should().Be(age); - person.Age = 13; - 13.Should().Be(age); - } - - [Theory, - InlineData(true), - InlineData(false)] - public void SubscribeToPropertyChangeForAllItemsInList( bool notifyOnInitialValue) + [Theory, InlineData(true), InlineData(false)] + public void SubscribeToPropertyChangeForAllItemsInList(bool notifyOnInitialValue) { - var lastChange = new PropertyValue(null, -1); + var lastChange = new PropertyValue(new Person(), -1); var source = new SourceList(); source.Connect().WhenPropertyChanged(p => p.Age, notifyOnInitialValue).Subscribe(c => lastChange = c); var person = new Person("Name", 10); @@ -64,7 +29,7 @@ public void SubscribeToPropertyChangeForAllItemsInList( bool notifyOnInitialValu } else { - lastChange.Sender.Should().BeNull(); + lastChange.Sender.Name.Should().Be("unknown"); (-1).Should().Be(lastChange.Value); } @@ -76,12 +41,10 @@ public void SubscribeToPropertyChangeForAllItemsInList( bool notifyOnInitialValu 13.Should().Be(lastChange.Value); } - [Theory, - InlineData(true), - InlineData(false)] - public void SubscribeToProperyChangedOnASingleItem( bool notifyOnInitialValue) + [Theory, InlineData(true), InlineData(false)] + public void SubscribeToProperyChangedOnASingleItem(bool notifyOnInitialValue) { - var lastChange = new PropertyValue(null, -1); + var lastChange = new PropertyValue(new Person(), -1); var person = new Person("Name", 10); person.WhenPropertyChanged(p => p.Age, notifyOnInitialValue).Subscribe(c => lastChange = c); @@ -92,7 +55,7 @@ public void SubscribeToProperyChangedOnASingleItem( bool notifyOnInitialValue) } else { - lastChange.Sender.Should().BeNull(); + lastChange.Sender.Name.Should().Be("unknown"); (-1).Should().Be(lastChange.Value); } @@ -104,5 +67,36 @@ public void SubscribeToProperyChangedOnASingleItem( bool notifyOnInitialValue) 13.Should().Be(lastChange.Value); } + [Theory, InlineData(true), InlineData(false)] + public void SubscribeToValueChangedOnASingleItem(bool notifyOnInitialValue) + { + var age = -1; + var person = new Person("Name", 10); + person.WhenValueChanged(p => p.Age, notifyOnInitialValue).Subscribe(i => age = i); + + (notifyOnInitialValue ? 10 : -1).Should().Be(age); + person.Age = 12; + 12.Should().Be(age); + person.Age = 13; + 13.Should().Be(age); + } + + [Theory, InlineData(true), InlineData(false)] + public void SubscribeToValueChangeForAllItemsInList(bool notifyOnInitialValue) + { + var lastAgeChange = -1; + var source = new SourceList(); + source.Connect().WhenValueChanged(p => p.Age, notifyOnInitialValue).Subscribe(i => lastAgeChange = i); + var person = new Person("Name", 10); + var anotherPerson = new Person("AnotherName", 10); + source.Add(person); + source.Add(anotherPerson); + + (notifyOnInitialValue ? 10 : -1).Should().Be(lastAgeChange); + person.Age = 12; + 12.Should().Be(lastAgeChange); + anotherPerson.Age = 13; + 13.Should().Be(lastAgeChange); + } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheFixture.cs b/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheFixture.cs index dbc633907..1737c413c 100644 --- a/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheFixture.cs +++ b/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheFixture.cs @@ -2,20 +2,25 @@ using System.Collections.Specialized; using System.Linq; using System.Reactive.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - - public class ObservableCollectionBindCacheFixture: IDisposable + public class ObservableCollectionBindCacheFixture : IDisposable { - private readonly ObservableCollectionExtended _collection = new ObservableCollectionExtended(); - private readonly ISourceCache _source; private readonly IDisposable _binder; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly ObservableCollectionExtended _collection = new(); + + private readonly RandomPersonGenerator _generator = new(); + + private readonly ISourceCache _source; public ObservableCollectionBindCacheFixture() { @@ -23,12 +28,6 @@ public ObservableCollectionBindCacheFixture() _binder = _source.Connect().Bind(_collection).Subscribe(); } - public void Dispose() - { - _binder.Dispose(); - _source.Dispose(); - } - [Fact] public void AddToSourceAddsToDestination() { @@ -40,34 +39,28 @@ public void AddToSourceAddsToDestination() } [Fact] - public void UpdateToSourceUpdatesTheDestination() + public void BatchAdd() { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.AddOrUpdate(person); - _source.AddOrUpdate(personUpdated); + var people = _generator.Take(100).ToList(); + _source.AddOrUpdate(people); - _collection.Count.Should().Be(1, "Should be 1 item in the collection"); - _collection.First().Should().Be(personUpdated, "Should be updated person"); + _collection.Count.Should().Be(100, "Should be 100 items in the collection"); + _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); } [Fact] - public void UpdateToSourceSendsReplaceOnDestination() + public void BatchRemove() { - var person = new Person("Adult1", 50); - var anotherPerson = new Person("Adult1", 51); - NotifyCollectionChangedAction action = default; - _source.AddOrUpdate(person); - - using (_collection - .ObserveCollectionChanges() - .Select(x => x.EventArgs.Action) - .Subscribe(updateType => action = updateType)) - { - _source.AddOrUpdate(anotherPerson); - } + var people = _generator.Take(100).ToList(); + _source.AddOrUpdate(people); + _source.Clear(); + _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + } - action.Should().Be(NotifyCollectionChangedAction.Replace, "The notification type should be Replace"); + public void Dispose() + { + _binder.Dispose(); + _source.Dispose(); } [Fact] @@ -81,22 +74,31 @@ public void RemoveSourceRemovesFromTheDestination() } [Fact] - public void BatchAdd() + public void UpdateToSourceSendsReplaceOnDestination() { - var people = _generator.Take(100).ToList(); - _source.AddOrUpdate(people); + var person = new Person("Adult1", 50); + var anotherPerson = new Person("Adult1", 51); + NotifyCollectionChangedAction action = default; + _source.AddOrUpdate(person); - _collection.Count.Should().Be(100, "Should be 100 items in the collection"); - _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); + using (_collection.ObserveCollectionChanges().Select(x => x.EventArgs.Action).Subscribe(updateType => action = updateType)) + { + _source.AddOrUpdate(anotherPerson); + } + + action.Should().Be(NotifyCollectionChangedAction.Replace, "The notification type should be Replace"); } [Fact] - public void BatchRemove() + public void UpdateToSourceUpdatesTheDestination() { - var people = _generator.Take(100).ToList(); - _source.AddOrUpdate(people); - _source.Clear(); - _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); + _source.AddOrUpdate(person); + _source.AddOrUpdate(personUpdated); + + _collection.Count.Should().Be(1, "Should be 1 item in the collection"); + _collection.First().Should().Be(personUpdated, "Should be updated person"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheSortedFixture.cs b/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheSortedFixture.cs index ec95c1287..35a99f622 100644 --- a/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheSortedFixture.cs +++ b/src/DynamicData.Tests/Binding/ObservableCollectionBindCacheSortedFixture.cs @@ -3,36 +3,33 @@ using System.Collections.Specialized; using System.Linq; using System.Reactive.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - - public class ObservableCollectionBindCacheSortedFixture: IDisposable + public class ObservableCollectionBindCacheSortedFixture : IDisposable { - private readonly ObservableCollectionExtended _collection = new ObservableCollectionExtended(); - private readonly ISourceCache _source; private readonly IDisposable _binder; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly ObservableCollectionExtended _collection = new ObservableCollectionExtended(); + private readonly IComparer _comparer = SortExpressionComparer.Ascending(p => p.Name); + private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly ISourceCache _source; + public ObservableCollectionBindCacheSortedFixture() { _collection = new ObservableCollectionExtended(); _source = new SourceCache(p => p.Name); - _binder = _source.Connect() - .Sort(_comparer, resetThreshold: 25) - .Bind(_collection) - .Subscribe(); - } - - public void Dispose() - { - _binder.Dispose(); - _source.Dispose(); + _binder = _source.Connect().Sort(_comparer, resetThreshold: 25).Bind(_collection).Subscribe(); } [Fact] @@ -45,28 +42,6 @@ public void AddToSourceAddsToDestination() _collection.First().Should().Be(person, "Should be same person"); } - [Fact] - public void UpdateToSourceUpdatesTheDestination() - { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.AddOrUpdate(person); - _source.AddOrUpdate(personUpdated); - - _collection.Count.Should().Be(1, "Should be 1 item in the collection"); - _collection.First().Should().Be(personUpdated, "Should be updated person"); - } - - [Fact] - public void RemoveSourceRemovesFromTheDestination() - { - var person = new Person("Adult1", 50); - _source.AddOrUpdate(person); - _source.Remove(person); - - _collection.Count.Should().Be(0, "Should be 1 item in the collection"); - } - [Fact] public void BatchAdd() { @@ -94,6 +69,12 @@ public void CollectionIsInSortOrder() sorted.Should().BeEquivalentTo(_collection.ToList()); } + public void Dispose() + { + _binder.Dispose(); + _source.Dispose(); + } + [Fact] public void LargeUpdateInvokesAReset() { @@ -102,15 +83,25 @@ public void LargeUpdateInvokesAReset() bool invoked = false; _collection.CollectionChanged += (sender, e) => - { - invoked = true; - e.Action.Should().Be(NotifyCollectionChangedAction.Reset); - }; + { + invoked = true; + e.Action.Should().Be(NotifyCollectionChangedAction.Reset); + }; _source.AddOrUpdate(_generator.Take(100)); invoked.Should().BeTrue(); } + [Fact] + public void RemoveSourceRemovesFromTheDestination() + { + var person = new Person("Adult1", 50); + _source.AddOrUpdate(person); + _source.Remove(person); + + _collection.Count.Should().Be(0, "Should be 1 item in the collection"); + } + [Fact] public void SmallChangeDoesNotInvokeReset() { @@ -120,84 +111,60 @@ public void SmallChangeDoesNotInvokeReset() bool invoked = false; bool resetInvoked = false; _collection.CollectionChanged += (sender, e) => - { - invoked = true; - if (e.Action == NotifyCollectionChangedAction.Reset) { - resetInvoked = true; - } - }; + invoked = true; + if (e.Action == NotifyCollectionChangedAction.Reset) + { + resetInvoked = true; + } + }; _source.AddOrUpdate(_generator.Take(24)); invoked.Should().BeTrue(); resetInvoked.Should().BeFalse(); } - [Fact] - public void TreatMovesAsRemoveAdd() - { - var cache = new SourceCache(p => p.Name); - - var people = Enumerable.Range(0,10).Select(age => new Person("Person" + age, age)).ToList(); - var importantGuy = people.First(); - cache.AddOrUpdate(people); - - ISortedChangeSet latestSetWithoutMoves = null; - ISortedChangeSet latestSetWithMoves = null; - - var boundList1 = new ObservableCollectionExtended(); - var boundList2 = new ObservableCollectionExtended(); - - using (cache.Connect() - .AutoRefresh(p => p.Age) - .Sort(SortExpressionComparer.Ascending(p => p.Age)) - .TreatMovesAsRemoveAdd() - .Bind(boundList1) - .Subscribe(set => latestSetWithoutMoves = set)) - - using (cache.Connect() - .AutoRefresh(p => p.Age) - .Sort(SortExpressionComparer.Ascending(p => p.Age)) - .Bind(boundList2) - .Subscribe(set => latestSetWithMoves = set)) - { - - importantGuy.Age = importantGuy.Age + 200; - - latestSetWithoutMoves.Removes.Should().Be(1); - latestSetWithoutMoves.Adds.Should().Be(1); - latestSetWithoutMoves.Moves.Should().Be(0); - latestSetWithoutMoves.Updates.Should().Be(0); - - latestSetWithMoves.Moves.Should().Be(1); - latestSetWithMoves.Updates.Should().Be(0); - latestSetWithMoves.Removes.Should().Be(0); - latestSetWithMoves.Adds.Should().Be(0); - } - } - [Fact] - public void UpdateToSourceSendsReplaceIfSortingIsNotAffected() + public void TreatMovesAsRemoveAdd() { - var person1 = new Person("Adult1", 10); - var person2 = new Person("Adult2", 11); + var cache = new SourceCache(p => p.Name); - NotifyCollectionChangedAction action = default; - _source.AddOrUpdate(person1); - _source.AddOrUpdate(person2); + var people = Enumerable.Range(0, 10).Select(age => new Person("Person" + age, age)).ToList(); + var importantGuy = people.First(); + cache.AddOrUpdate(people); - var person2Updated = new Person("Adult2", 12); + ISortedChangeSet? latestSetWithoutMoves = null; + ISortedChangeSet? latestSetWithMoves = null; - using (_collection - .ObserveCollectionChanges() - .Select(change => change.EventArgs.Action) - .Subscribe(act => action = act)) + var boundList1 = new ObservableCollectionExtended(); + var boundList2 = new ObservableCollectionExtended(); + + using (cache.Connect().AutoRefresh(p => p.Age).Sort(SortExpressionComparer.Ascending(p => p.Age)).TreatMovesAsRemoveAdd().Bind(boundList1).Subscribe(set => latestSetWithoutMoves = set)) + + using (cache.Connect().AutoRefresh(p => p.Age).Sort(SortExpressionComparer.Ascending(p => p.Age)).Bind(boundList2).Subscribe(set => latestSetWithMoves = set)) { - _source.AddOrUpdate(person2Updated); - } + importantGuy.Age += 200; - action.Should().Be(NotifyCollectionChangedAction.Replace, "The notification type should be Replace"); - _collection.Should().Equal(person1, person2Updated); + if (latestSetWithoutMoves is null) + { + throw new InvalidOperationException(nameof(latestSetWithoutMoves)); + } + + if (latestSetWithMoves is null) + { + throw new InvalidOperationException(nameof(latestSetWithMoves)); + } + + latestSetWithoutMoves.Removes.Should().Be(1); + latestSetWithoutMoves.Adds.Should().Be(1); + latestSetWithoutMoves.Moves.Should().Be(0); + latestSetWithoutMoves.Updates.Should().Be(0); + + latestSetWithMoves.Moves.Should().Be(1); + latestSetWithMoves.Updates.Should().Be(0); + latestSetWithMoves.Removes.Should().Be(0); + latestSetWithMoves.Adds.Should().Be(0); + } } [Fact] @@ -211,18 +178,12 @@ public void UpdateToSourceSendsRemoveAndAddIfSortingIsAffected() var collection = new ObservableCollectionExtended(); using (var source = new SourceCache(person => person.Name)) - using (source.Connect() - .Sort(SortExpressionComparer.Ascending(person => person.Age)) - .Bind(collection) - .Subscribe()) + using (source.Connect().Sort(SortExpressionComparer.Ascending(person => person.Age)).Bind(collection).Subscribe()) { source.AddOrUpdate(person1); source.AddOrUpdate(person2); - using (collection - .ObserveCollectionChanges() - .Select(change => change.EventArgs.Action) - .Subscribe(act => actions.Add(act))) + using (collection.ObserveCollectionChanges().Select(change => change.EventArgs.Action).Subscribe(act => actions.Add(act))) { source.AddOrUpdate(person2Updated); } @@ -231,5 +192,38 @@ public void UpdateToSourceSendsRemoveAndAddIfSortingIsAffected() actions.Should().Equal(NotifyCollectionChangedAction.Remove, NotifyCollectionChangedAction.Add); collection.Should().Equal(person2Updated, person1); } + + [Fact] + public void UpdateToSourceSendsReplaceIfSortingIsNotAffected() + { + var person1 = new Person("Adult1", 10); + var person2 = new Person("Adult2", 11); + + NotifyCollectionChangedAction action = default; + _source.AddOrUpdate(person1); + _source.AddOrUpdate(person2); + + var person2Updated = new Person("Adult2", 12); + + using (_collection.ObserveCollectionChanges().Select(change => change.EventArgs.Action).Subscribe(act => action = act)) + { + _source.AddOrUpdate(person2Updated); + } + + action.Should().Be(NotifyCollectionChangedAction.Replace, "The notification type should be Replace"); + _collection.Should().Equal(person1, person2Updated); + } + + [Fact] + public void UpdateToSourceUpdatesTheDestination() + { + var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); + _source.AddOrUpdate(person); + _source.AddOrUpdate(personUpdated); + + _collection.Count.Should().Be(1, "Should be 1 item in the collection"); + _collection.First().Should().Be(personUpdated, "Should be updated person"); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/ObservableCollectionBindListFixture.cs b/src/DynamicData.Tests/Binding/ObservableCollectionBindListFixture.cs index 8f8402a8e..ca0dbbbcd 100644 --- a/src/DynamicData.Tests/Binding/ObservableCollectionBindListFixture.cs +++ b/src/DynamicData.Tests/Binding/ObservableCollectionBindListFixture.cs @@ -1,19 +1,24 @@ using System; using System.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - - public class ObservableCollectionBindListFixture: IDisposable + public class ObservableCollectionBindListFixture : IDisposable { - private readonly ObservableCollectionExtended _collection = new ObservableCollectionExtended(); - private readonly SourceList _source; private readonly IDisposable _binder; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly ObservableCollectionExtended _collection = new(); + + private readonly RandomPersonGenerator _generator = new(); + + private readonly SourceList _source; public ObservableCollectionBindListFixture() { @@ -22,10 +27,14 @@ public ObservableCollectionBindListFixture() _binder = _source.Connect().Bind(_collection).Subscribe(); } - public void Dispose() + [Fact] + public void AddRange() { - _binder.Dispose(); - _source.Dispose(); + var people = _generator.Take(100).ToList(); + _source.AddRange(people); + + _collection.Count.Should().Be(100, "Should be 100 items in the collection"); + _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); } [Fact] @@ -39,15 +48,18 @@ public void AddToSourceAddsToDestination() } [Fact] - public void UpdateToSourceUpdatesTheDestination() + public void Clear() { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.Add(person); - _source.Replace(person, personUpdated); + var people = _generator.Take(100).ToList(); + _source.AddRange(people); + _source.Clear(); + _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + } - _collection.Count.Should().Be(1, "Should be 1 item in the collection"); - _collection.First().Should().Be(personUpdated, "Should be updated person"); + public void Dispose() + { + _binder.Dispose(); + _source.Dispose(); } [Fact] @@ -61,22 +73,15 @@ public void RemoveSourceRemovesFromTheDestination() } [Fact] - public void AddRange() + public void UpdateToSourceUpdatesTheDestination() { - var people = _generator.Take(100).ToList(); - _source.AddRange(people); - - _collection.Count.Should().Be(100, "Should be 100 items in the collection"); - _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); - } + var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); + _source.Add(person); + _source.Replace(person, personUpdated); - [Fact] - public void Clear() - { - var people = _generator.Take(100).ToList(); - _source.AddRange(people); - _source.Clear(); - _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + _collection.Count.Should().Be(1, "Should be 1 item in the collection"); + _collection.First().Should().Be(personUpdated, "Should be updated person"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/ObservableCollectionExtendedToChangeSetFixture.cs b/src/DynamicData.Tests/Binding/ObservableCollectionExtendedToChangeSetFixture.cs index 783d43285..8d100b81d 100644 --- a/src/DynamicData.Tests/Binding/ObservableCollectionExtendedToChangeSetFixture.cs +++ b/src/DynamicData.Tests/Binding/ObservableCollectionExtendedToChangeSetFixture.cs @@ -1,17 +1,21 @@ using System; using System.Collections.ObjectModel; using System.Linq; + using DynamicData.Binding; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - - public class ObservableCollectionExtendedToChangeSetFixture: IDisposable + public class ObservableCollectionExtendedToChangeSetFixture : IDisposable { private readonly ObservableCollectionExtended _collection; + private readonly ChangeSetAggregator _results; + private readonly ReadOnlyObservableCollection _target; public ObservableCollectionExtendedToChangeSetFixture() @@ -21,11 +25,30 @@ public ObservableCollectionExtendedToChangeSetFixture() _results = _target.ToObservableChangeSet().AsAggregator(); } + [Fact] + public void Add() + { + _collection.Add(1); + + _results.Messages.Count.Should().Be(1); + _results.Data.Count.Should().Be(1); + _results.Data.Items.First().Should().Be(1); + } + public void Dispose() { _results.Dispose(); } + [Fact] + public void Duplicates() + { + _collection.Add(1); + _collection.Add(1); + + _results.Data.Count.Should().Be(2); + } + [Fact] public void Move() { @@ -39,16 +62,6 @@ public void Move() _results.Data.Items.Should().BeEquivalentTo(_target); } - [Fact] - public void Add() - { - _collection.Add(1); - - _results.Messages.Count.Should().Be(1); - _results.Data.Count.Should().Be(1); - _results.Data.Items.First().Should().Be(1); - } - [Fact] public void Remove() { @@ -61,15 +74,6 @@ public void Remove() _results.Data.Items.Should().BeEquivalentTo(_target); } - [Fact] - public void Duplicates() - { - _collection.Add(1); - _collection.Add(1); - - _results.Data.Count.Should().Be(2); - } - [Fact] public void Replace() { @@ -77,7 +81,6 @@ public void Replace() _collection[8] = 20; _results.Data.Items.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 20, 10 }); - } //[Fact] diff --git a/src/DynamicData.Tests/Binding/ObservableCollectionToChangeSetFixture.cs b/src/DynamicData.Tests/Binding/ObservableCollectionToChangeSetFixture.cs index a62c89eb8..4b81ca7b4 100644 --- a/src/DynamicData.Tests/Binding/ObservableCollectionToChangeSetFixture.cs +++ b/src/DynamicData.Tests/Binding/ObservableCollectionToChangeSetFixture.cs @@ -2,16 +2,19 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; + using DynamicData.Binding; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - - public class ObservableCollectionToChangeSetFixture: IDisposable + public class ObservableCollectionToChangeSetFixture : IDisposable { private readonly TestObservableCollection _collection; + private readonly ChangeSetAggregator _results; public ObservableCollectionToChangeSetFixture() @@ -20,11 +23,30 @@ public ObservableCollectionToChangeSetFixture() _results = _collection.ToObservableChangeSet().AsAggregator(); } + [Fact] + public void Add() + { + _collection.Add(1); + + _results.Messages.Count.Should().Be(1); + _results.Data.Count.Should().Be(1); + _results.Data.Items.First().Should().Be(1); + } + public void Dispose() { _results.Dispose(); } + [Fact] + public void Duplicates() + { + _collection.Add(1); + _collection.Add(1); + + _results.Data.Count.Should().Be(2); + } + [Fact] public void Move() { @@ -38,16 +60,6 @@ public void Move() _results.Data.Items.Should().BeEquivalentTo(_collection); } - [Fact] - public void Add() - { - _collection.Add(1); - - _results.Messages.Count.Should().Be(1); - _results.Data.Count.Should().Be(1); - _results.Data.Items.First().Should().Be(1); - } - [Fact] public void Remove() { @@ -60,23 +72,13 @@ public void Remove() _results.Data.Items.Should().BeEquivalentTo(_collection); } - [Fact] - public void Duplicates() - { - _collection.Add(1); - _collection.Add(1); - - _results.Data.Count.Should().Be(2); - } - [Fact] public void Replace() { _collection.AddRange(Enumerable.Range(1, 10)); _collection[8] = 20; - _results.Data.Items.Should().BeEquivalentTo(new []{1,2,3,4,5,6,7,8,20,10}); - + _results.Data.Items.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 20, 10 }); } [Fact] @@ -100,4 +102,4 @@ public void Reset() } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Binding/ReadOnlyObservableCollectionToChangeSetFixture.cs b/src/DynamicData.Tests/Binding/ReadOnlyObservableCollectionToChangeSetFixture.cs index 858808b7a..4c4948220 100644 --- a/src/DynamicData.Tests/Binding/ReadOnlyObservableCollectionToChangeSetFixture.cs +++ b/src/DynamicData.Tests/Binding/ReadOnlyObservableCollectionToChangeSetFixture.cs @@ -2,18 +2,21 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; -using System.Reactive.Linq; + using DynamicData.Binding; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Binding { - - public class ReadOnlyObservableCollectionToChangeSetFixture: IDisposable + public class ReadOnlyObservableCollectionToChangeSetFixture : IDisposable { private readonly TestObservableCollection _collection; + private readonly ChangeSetAggregator _results; + private readonly ReadOnlyObservableCollection _target; public ReadOnlyObservableCollectionToChangeSetFixture() @@ -23,24 +26,6 @@ public ReadOnlyObservableCollectionToChangeSetFixture() _results = _target.ToObservableChangeSet().AsAggregator(); } - public void Dispose() - { - _results.Dispose(); - } - - [Fact] - public void Move() - { - _collection.AddRange(Enumerable.Range(1, 10)); - - _results.Data.Items.Should().BeEquivalentTo(_target); - _collection.Move(5, 8); - _results.Data.Items.Should().BeEquivalentTo(_target); - - _collection.Move(7, 1); - _results.Data.Items.Should().BeEquivalentTo(_target); - } - [Fact] public void Add() { @@ -51,16 +36,9 @@ public void Add() _results.Data.Items.First().Should().Be(1); } - [Fact] - public void Remove() + public void Dispose() { - _collection.AddRange(Enumerable.Range(1, 10)); - - _collection.Remove(3); - - _results.Data.Count.Should().Be(9); - _results.Data.Items.Contains(3).Should().BeFalse(); - _results.Data.Items.Should().BeEquivalentTo(_target); + _results.Dispose(); } [Fact] @@ -73,25 +51,16 @@ public void Duplicates() } [Fact] - public void Replace() - { - _collection.AddRange(Enumerable.Range(1, 10)); - _collection[8] = 20; - - _results.Data.Items.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 20, 10 }); - } - - [Fact] - public void ResetFiresClearsAndAdds() + public void Move() { _collection.AddRange(Enumerable.Range(1, 10)); - _collection.Reset(); + _results.Data.Items.Should().BeEquivalentTo(_target); + _collection.Move(5, 8); _results.Data.Items.Should().BeEquivalentTo(_target); - var resetNotification = _results.Messages.Last(); - resetNotification.Removes.Should().Be(10); - resetNotification.Adds.Should().Be(10); + _collection.Move(7, 1); + _results.Data.Items.Should().BeEquivalentTo(_target); } [Fact] @@ -100,15 +69,11 @@ public void RefreshNotSupported() // Arrange var sourceCache = new SourceCache(item => item.Id); - var item1 = new Item(name: "Old Name"); + var item1 = new Item("Old Name"); sourceCache.AddOrUpdate(item1); - var sourceCacheResults = sourceCache - .Connect() - .AutoRefresh(item => item.Name) - .Bind(out var collection) - .AsAggregator(); + var sourceCacheResults = sourceCache.Connect().AutoRefresh(item => item.Name).Bind(out var collection).AsAggregator(); var collectionResults = collection.ToObservableChangeSet().AsAggregator(); @@ -130,6 +95,40 @@ public void RefreshNotSupported() collectionResults.Dispose(); } + [Fact] + public void Remove() + { + _collection.AddRange(Enumerable.Range(1, 10)); + + _collection.Remove(3); + + _results.Data.Count.Should().Be(9); + _results.Data.Items.Contains(3).Should().BeFalse(); + _results.Data.Items.Should().BeEquivalentTo(_target); + } + + [Fact] + public void Replace() + { + _collection.AddRange(Enumerable.Range(1, 10)); + _collection[8] = 20; + + _results.Data.Items.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 20, 10 }); + } + + [Fact] + public void ResetFiresClearsAndAdds() + { + _collection.AddRange(Enumerable.Range(1, 10)); + + _collection.Reset(); + _results.Data.Items.Should().BeEquivalentTo(_target); + + var resetNotification = _results.Messages.Last(); + resetNotification.Removes.Should().Be(10); + resetNotification.Adds.Should().Be(10); + } + private class TestObservableCollection : ObservableCollection { public void Reset() diff --git a/src/DynamicData.Tests/Cache/AndFixture.cs b/src/DynamicData.Tests/Cache/AndFixture.cs index a5b3257cf..0199ccab1 100644 --- a/src/DynamicData.Tests/Cache/AndFixture.cs +++ b/src/DynamicData.Tests/Cache/AndFixture.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class AndFixture : AndFixtureBase { protected override IObservable> CreateObservable() @@ -25,11 +27,13 @@ protected override IObservable> CreateObservable() } } - public abstract class AndFixtureBase: IDisposable + public abstract class AndFixtureBase : IDisposable { protected ISourceCache _source1; + protected ISourceCache _source2; - private ChangeSetAggregator _results; + + private readonly ChangeSetAggregator _results; protected AndFixtureBase() { @@ -38,8 +42,6 @@ protected AndFixtureBase() _results = CreateObservable().AsAggregator(); } - protected abstract IObservable> CreateObservable(); - public void Dispose() { _source1.Dispose(); @@ -48,36 +50,37 @@ public void Dispose() } [Fact] - public void UpdatingOneSourceOnlyProducesNoResults() + public void RemovingFromOneRemovesFromResult() { var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); + _source2.AddOrUpdate(person); - _results.Messages.Count.Should().Be(0, "Should have no updates"); + _source2.Remove(person); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); _results.Data.Count.Should().Be(0, "Cache should have no items"); } [Fact] - public void UpdatingBothProducesResults() + public void StartingWithNonEmptySourceProducesNoResult() { - var person = new Person("Adult1", 50); + var person = new Person("Adult", 50); _source1.AddOrUpdate(person); - _source2.AddOrUpdate(person); - _results.Messages.Count.Should().Be(1, "Should have no updates"); - _results.Data.Count.Should().Be(1, "Cache should have no items"); - _results.Data.Items.First().Should().Be(person, "Should be same person"); + + using var result = CreateObservable().AsAggregator(); + _results.Messages.Count.Should().Be(0, "Should have no updates"); + result.Data.Count.Should().Be(0, "Cache should have no items"); } [Fact] - public void RemovingFromOneRemovesFromResult() + public void UpdatingBothProducesResults() { var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - - _source2.Remove(person); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Data.Count.Should().Be(0, "Cache should have no items"); + _results.Messages.Count.Should().Be(1, "Should have no updates"); + _results.Data.Count.Should().Be(1, "Cache should have no items"); + _results.Data.Items.First().Should().Be(person, "Should be same person"); } [Fact] @@ -95,16 +98,15 @@ public void UpdatingOneProducesOnlyOneUpdate() } [Fact] - public void StartingWithNonEmptySourceProducesNoResult() + public void UpdatingOneSourceOnlyProducesNoResults() { - var person = new Person("Adult", 50); + var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); - using (var result = CreateObservable().AsAggregator()) - { - _results.Messages.Count.Should().Be(0, "Should have no updates"); - result.Data.Count.Should().Be(0, "Cache should have no items"); - } + _results.Messages.Count.Should().Be(0, "Should have no updates"); + _results.Data.Count.Should().Be(0, "Cache should have no items"); } + + protected abstract IObservable> CreateObservable(); } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/AutoRefreshFixture.cs b/src/DynamicData.Tests/Cache/AutoRefreshFixture.cs index 7c4dda6a5..996356fae 100644 --- a/src/DynamicData.Tests/Cache/AutoRefreshFixture.cs +++ b/src/DynamicData.Tests/Cache/AutoRefreshFixture.cs @@ -2,110 +2,100 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class AutoRefreshFixture { [Fact] public void AutoRefresh() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, 1)) - .ToArray(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, 1)).ToArray(); //result should only be true when all items are set to true - using (var cache = new SourceCache(m => m.Name)) - using (var results = cache.Connect().AutoRefresh(p=>p.Age).AsAggregator()) - { - cache.AddOrUpdate(items); - - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(1); - - items[0].Age = 10; - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(2); - - results.Messages[1].First().Reason.Should().Be(ChangeReason.Refresh); - - //remove an item and check no change is fired - var toRemove = items[1]; - cache.Remove(toRemove); - results.Data.Count.Should().Be(99); - results.Messages.Count.Should().Be(3); - toRemove.Age = 100; - results.Messages.Count.Should().Be(3); - - //add it back in and check it updates - cache.AddOrUpdate(toRemove); - results.Messages.Count.Should().Be(4); - toRemove.Age = 101; - results.Messages.Count.Should().Be(5); - - results.Messages.Last().First().Reason.Should().Be(ChangeReason.Refresh); - } + using var cache = new SourceCache(m => m.Name); + using var results = cache.Connect().AutoRefresh(p => p.Age).AsAggregator(); + cache.AddOrUpdate(items); + + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(1); + + items[0].Age = 10; + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(2); + + results.Messages[1].First().Reason.Should().Be(ChangeReason.Refresh); + + //remove an item and check no change is fired + var toRemove = items[1]; + cache.Remove(toRemove); + results.Data.Count.Should().Be(99); + results.Messages.Count.Should().Be(3); + toRemove.Age = 100; + results.Messages.Count.Should().Be(3); + + //add it back in and check it updates + cache.AddOrUpdate(toRemove); + results.Messages.Count.Should().Be(4); + toRemove.Age = 101; + results.Messages.Count.Should().Be(5); + + results.Messages.Last().First().Reason.Should().Be(ChangeReason.Refresh); } [Fact] public void AutoRefreshFromObservable() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, 1)) - .ToArray(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, 1)).ToArray(); //result should only be true when all items are set to true - using (var cache = new SourceCache(m => m.Name)) - using (var results = cache.Connect().AutoRefreshOnObservable(p => p.WhenAnyPropertyChanged()).AsAggregator()) - { - cache.AddOrUpdate(items); - - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(1); - - items[0].Age = 10; - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(2); - - results.Messages[1].First().Reason.Should().Be(ChangeReason.Refresh); - - //remove an item and check no change is fired - var toRemove = items[1]; - cache.Remove(toRemove); - results.Data.Count.Should().Be(99); - results.Messages.Count.Should().Be(3); - toRemove.Age = 100; - results.Messages.Count.Should().Be(3); - - //add it back in and check it updates - cache.AddOrUpdate(toRemove); - results.Messages.Count.Should().Be(4); - toRemove.Age = 101; - results.Messages.Count.Should().Be(5); - - results.Messages.Last().First().Reason.Should().Be(ChangeReason.Refresh); - } + using var cache = new SourceCache(m => m.Name); + using var results = cache.Connect().AutoRefreshOnObservable(p => p.WhenAnyPropertyChanged()).AsAggregator(); + cache.AddOrUpdate(items); + + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(1); + + items[0].Age = 10; + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(2); + + results.Messages[1].First().Reason.Should().Be(ChangeReason.Refresh); + + //remove an item and check no change is fired + var toRemove = items[1]; + cache.Remove(toRemove); + results.Data.Count.Should().Be(99); + results.Messages.Count.Should().Be(3); + toRemove.Age = 100; + results.Messages.Count.Should().Be(3); + + //add it back in and check it updates + cache.AddOrUpdate(toRemove); + results.Messages.Count.Should().Be(4); + toRemove.Age = 101; + results.Messages.Count.Should().Be(5); + + results.Messages.Last().First().Reason.Should().Be(ChangeReason.Refresh); } [Fact] public void MakeSelectMagicWorkWithObservable() { - var initialItem = new IntHolder() { Value = 1, Description = "Initial Description" }; + var initialItem = new IntHolder { Value = 1, Description = "Initial Description" }; var sourceList = new SourceList(); sourceList.Add(initialItem); - var descriptionStream = sourceList - .Connect() - .AutoRefresh(intHolder => intHolder.Description) - .Transform(intHolder => intHolder.Description, true) - .Do(x => { }) // <--- Add break point here to check the overload fixes it - .Bind(out ReadOnlyObservableCollection resultCollection); + var descriptionStream = sourceList.Connect().AutoRefresh(intHolder => intHolder!.Description).Transform(intHolder => intHolder!.Description, true).Do(x => { }) // <--- Add break point here to check the overload fixes it + .Bind(out ReadOnlyObservableCollection resultCollection); using (descriptionStream.Subscribe()) { @@ -119,20 +109,21 @@ public void MakeSelectMagicWorkWithObservable() public class IntHolder : AbstractNotifyPropertyChanged { + public string? _description_; + public int _value; - public int Value - { - get => _value; - set => SetAndRaise(ref _value, value); - } - public string _description_; - public string Description + public string? Description { get => _description_; set => SetAndRaise(ref _description_, value); } - } + public int Value + { + get => _value; + set => SetAndRaise(ref _value, value); + } + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/BatchFixture.cs b/src/DynamicData.Tests/Cache/BatchFixture.cs index 791640dac..490deefc8 100644 --- a/src/DynamicData.Tests/Cache/BatchFixture.cs +++ b/src/DynamicData.Tests/Cache/BatchFixture.cs @@ -1,19 +1,24 @@ using System; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { - - public class BatchFixture: IDisposable + public class BatchFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; - public BatchFixture() + private readonly ISourceCache _source; + + public BatchFixture() { _scheduler = new TestScheduler(); _source = new SourceCache(p => p.Key); @@ -43,4 +48,4 @@ public void ResultsWillBeReceivedAfterClosingBuffer() _results.Messages.Count.Should().Be(1, "Should be 1 update"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/BatchIfFixture.cs b/src/DynamicData.Tests/Cache/BatchIfFixture.cs index 209f5da88..16e8ebb00 100644 --- a/src/DynamicData.Tests/Cache/BatchIfFixture.cs +++ b/src/DynamicData.Tests/Cache/BatchIfFixture.cs @@ -1,33 +1,70 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { - public class BatchIfFixture: IDisposable + public class BatchIfFixture : IDisposable { - private readonly ISourceCache _source; + private readonly ISubject _pausingSubject = new Subject(); + private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; - private readonly ISubject _pausingSubject = new Subject(); - public BatchIfFixture() + private readonly ISourceCache _source; + + public BatchIfFixture() { _scheduler = new TestScheduler(); _source = new SourceCache(p => p.Key); _results = _source.Connect().BatchIf(_pausingSubject, _scheduler).AsAggregator(); - // _results = _source.Connect().BatchIf(new BehaviorSubject(true), scheduler: _scheduler).AsAggregator(); + // _results = _source.Connect().BatchIf(new BehaviorSubject(true), scheduler: _scheduler).AsAggregator(); } - public void Dispose() + [Fact] + public void CanToggleSuspendResume() { - _results.Dispose(); - _source.Dispose(); + _pausingSubject.OnNext(true); + ////advance otherwise nothing happens + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + + _source.AddOrUpdate(new Person("A", 1)); + + //go forward an arbitary amount of time + _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); + _results.Messages.Count.Should().Be(0, "There should be no messages"); + + _pausingSubject.OnNext(false); + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + + _source.AddOrUpdate(new Person("B", 1)); + + _results.Messages.Count.Should().Be(2, "There should be 2 messages"); + + _pausingSubject.OnNext(true); + ////advance otherwise nothing happens + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + + _source.AddOrUpdate(new Person("C", 1)); + + //go forward an arbitary amount of time + _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); + _results.Messages.Count.Should().Be(2, "There should be 2 messages"); + + _pausingSubject.OnNext(false); + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + + _results.Messages.Count.Should().Be(3, "There should be 3 messages"); } /// @@ -36,16 +73,11 @@ public void Dispose() [Fact] public void ChangesNotLostIfConsumerIsRunningOnDifferentThread() { - var producerScheduler = new TestScheduler(); var consumerScheduler = new TestScheduler(); //Note consumer is running on a different scheduler - _source.Connect() - .BatchIf(_pausingSubject, producerScheduler) - .ObserveOn(consumerScheduler) - .Bind(out var target) - .AsAggregator(); + _source.Connect().BatchIf(_pausingSubject, producerScheduler).ObserveOn(consumerScheduler).Bind(out var target).AsAggregator(); _source.AddOrUpdate(new Person("A", 1)); @@ -79,6 +111,12 @@ public void ChangesNotLostIfConsumerIsRunningOnDifferentThread() target.Count.Should().Be(2, "There should be 2 message"); } + public void Dispose() + { + _results.Dispose(); + _source.Dispose(); + } + [Fact] public void NoResultsWillBeReceivedIfPaused() { @@ -102,41 +140,5 @@ public void ResultsWillBeReceivedIfNotPaused() _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); _results.Messages.Count.Should().Be(1, "Should be 1 update"); } - - [Fact] - public void CanToggleSuspendResume() - { - _pausingSubject.OnNext(true); - ////advance otherwise nothing happens - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - - _source.AddOrUpdate(new Person("A", 1)); - - //go forward an arbitary amount of time - _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); - _results.Messages.Count.Should().Be(0, "There should be no messages"); - - _pausingSubject.OnNext(false); - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - - _source.AddOrUpdate(new Person("B", 1)); - - _results.Messages.Count.Should().Be(2, "There should be 2 messages"); - - _pausingSubject.OnNext(true); - ////advance otherwise nothing happens - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - - _source.AddOrUpdate(new Person("C", 1)); - - //go forward an arbitary amount of time - _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); - _results.Messages.Count.Should().Be(2, "There should be 2 messages"); - - _pausingSubject.OnNext(false); - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - - _results.Messages.Count.Should().Be(3, "There should be 3 messages"); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/BatchIfWithTimeOutFixture.cs b/src/DynamicData.Tests/Cache/BatchIfWithTimeOutFixture.cs index 5e2364ba2..5d3b8f957 100644 --- a/src/DynamicData.Tests/Cache/BatchIfWithTimeOutFixture.cs +++ b/src/DynamicData.Tests/Cache/BatchIfWithTimeOutFixture.cs @@ -2,99 +2,102 @@ using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { public class BatchIfWithTimeoutFixture : IDisposable { - private readonly ISourceCache _source; private readonly TestScheduler _scheduler; + private readonly ISourceCache _source; + public BatchIfWithTimeoutFixture() { _scheduler = new TestScheduler(); _source = new SourceCache(p => p.Key); } + public void Dispose() + { + _source.Dispose(); + } + [Fact] public void InitialPause() { var pausingSubject = new Subject(); - using (var results = _source.Connect().BatchIf(pausingSubject, true, _scheduler).AsAggregator()) - { - // no results because the initial pause state is pause - _source.AddOrUpdate(new Person("A", 1)); - results.Data.Count.Should().Be(0); - - //resume and expect a result - pausingSubject.OnNext(false); - results.Data.Count.Should().Be(1); - - //add another in the window where there is no pause - _source.AddOrUpdate(new Person("B", 1)); - results.Data.Count.Should().Be(2); - - // pause again - pausingSubject.OnNext(true); - _source.AddOrUpdate(new Person("C", 1)); - results.Data.Count.Should().Be(2); - - //resume for the second time - pausingSubject.OnNext(false); - results.Data.Count.Should().Be(3); - } + using var results = _source.Connect().BatchIf(pausingSubject, true, _scheduler).AsAggregator(); + // no results because the initial pause state is pause + _source.AddOrUpdate(new Person("A", 1)); + results.Data.Count.Should().Be(0); + + //resume and expect a result + pausingSubject.OnNext(false); + results.Data.Count.Should().Be(1); + + //add another in the window where there is no pause + _source.AddOrUpdate(new Person("B", 1)); + results.Data.Count.Should().Be(2); + + // pause again + pausingSubject.OnNext(true); + _source.AddOrUpdate(new Person("C", 1)); + results.Data.Count.Should().Be(2); + + //resume for the second time + pausingSubject.OnNext(false); + results.Data.Count.Should().Be(3); } [Fact] public void Timeout() { var pausingSubject = new Subject(); - using (var results = _source.Connect().BatchIf(pausingSubject, TimeSpan.FromSeconds(1), _scheduler).AsAggregator()) - { - // no results because the initial pause state is pause - _source.AddOrUpdate(new Person("A", 1)); - results.Data.Count.Should().Be(1); - - // pause and add - pausingSubject.OnNext(true); - _source.AddOrUpdate(new Person("B", 1)); - results.Data.Count.Should().Be(1); - - //resume before timeout ends - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(500).Ticks); - results.Data.Count.Should().Be(1); - - pausingSubject.OnNext(false); - results.Data.Count.Should().Be(2); - - //pause and advance past timeout window - pausingSubject.OnNext(true); - _source.AddOrUpdate(new Person("C", 1)); - _scheduler.AdvanceBy(TimeSpan.FromSeconds(2.1).Ticks); - results.Data.Count.Should().Be(3); - - _source.AddOrUpdate(new Person("D", 1)); - results.Data.Count.Should().Be(4); - } - } + using var results = _source.Connect().BatchIf(pausingSubject, TimeSpan.FromSeconds(1), _scheduler).AsAggregator(); + // no results because the initial pause state is pause + _source.AddOrUpdate(new Person("A", 1)); + results.Data.Count.Should().Be(1); - public void Dispose() - { - _source.Dispose(); - } + // pause and add + pausingSubject.OnNext(true); + _source.AddOrUpdate(new Person("B", 1)); + results.Data.Count.Should().Be(1); + + //resume before timeout ends + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(500).Ticks); + results.Data.Count.Should().Be(1); + + pausingSubject.OnNext(false); + results.Data.Count.Should().Be(2); + + //pause and advance past timeout window + pausingSubject.OnNext(true); + _source.AddOrUpdate(new Person("C", 1)); + _scheduler.AdvanceBy(TimeSpan.FromSeconds(2.1).Ticks); + results.Data.Count.Should().Be(3); + _source.AddOrUpdate(new Person("D", 1)); + results.Data.Count.Should().Be(4); + } } public class BatchIfWithTimeOutFixture : IDisposable { - private readonly ISourceCache _source; + private readonly ISubject _pausingSubject = new Subject(); + private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; - private readonly ISubject _pausingSubject = new Subject(); + + private readonly ISourceCache _source; public BatchIfWithTimeOutFixture() { @@ -103,72 +106,49 @@ public BatchIfWithTimeOutFixture() _results = _source.Connect().BatchIf(_pausingSubject, TimeSpan.FromMinutes(1), _scheduler).AsAggregator(); } - public void Dispose() - { - _results.Dispose(); - _source.Dispose(); - _pausingSubject.OnCompleted(); - } - [Fact] - public void WillApplyTimeout() + public void CanToggleSuspendResume() { _pausingSubject.OnNext(true); - - //should timeout - _scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); + ////advance otherwise nothing happens + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); _source.AddOrUpdate(new Person("A", 1)); - _results.Messages.Count.Should().Be(1, "There should be 1 messages"); - } + //go forward an arbitary amount of time + _results.Messages.Count.Should().Be(0, "There should be no messages"); - [Fact] - public void NoResultsWillBeReceivedIfPaused() - { - _pausingSubject.OnNext(true); - //advance otherwise nothing happens + _pausingSubject.OnNext(false); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - _source.AddOrUpdate(new Person("A", 1)); + _source.AddOrUpdate(new Person("B", 1)); - _results.Messages.Count.Should().Be(0, "There should be no messages"); + _results.Messages.Count.Should().Be(2, "There should be 2 messages"); } - [Fact] - public void ResultsWillBeReceivedIfNotPaused() + public void Dispose() { - _source.AddOrUpdate(new Person("A", 1)); - - //go forward an arbitary amount of time - _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); - _results.Messages.Count.Should().Be(1, "Should be 1 update"); + _results.Dispose(); + _source.Dispose(); + _pausingSubject.OnCompleted(); } [Fact] - public void CanToggleSuspendResume() + public void NoResultsWillBeReceivedIfPaused() { _pausingSubject.OnNext(true); - ////advance otherwise nothing happens + //advance otherwise nothing happens _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); _source.AddOrUpdate(new Person("A", 1)); - //go forward an arbitary amount of time _results.Messages.Count.Should().Be(0, "There should be no messages"); - - _pausingSubject.OnNext(false); - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - - _source.AddOrUpdate(new Person("B", 1)); - - _results.Messages.Count.Should().Be(2, "There should be 2 messages"); } [Fact] - public void PublishesOnTimerCompletion() + public void PublishesOnIntervalEvent() { - var intervalTimer = Observable.Timer(TimeSpan.FromMilliseconds(5), _scheduler).Select(_ => Unit.Default); + var intervalTimer = Observable.Interval(TimeSpan.FromMilliseconds(5), _scheduler).Select(_ => Unit.Default); var results = _source.Connect().BatchIf(_pausingSubject, true, intervalTimer, _scheduler).AsAggregator(); //Buffering @@ -176,25 +156,33 @@ public void PublishesOnTimerCompletion() _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1).Ticks); results.Messages.Count.Should().Be(0, "There should be 0 messages"); - //Timer should event, buffered items delivered + //Interval Fires and drains buffer _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(5).Ticks); results.Messages.Count.Should().Be(1, "There should be 1 messages"); - //Unbuffered from here + //Buffering again _source.AddOrUpdate(new Person("B", 2)); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1).Ticks); + results.Messages.Count.Should().Be(1, "There should be 1 messages"); + + //Interval Fires and drains buffer + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(5).Ticks); results.Messages.Count.Should().Be(2, "There should be 2 messages"); - //Unbuffered from here + //Buffering again _source.AddOrUpdate(new Person("C", 3)); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1).Ticks); + results.Messages.Count.Should().Be(2, "There should be 2 messages"); + + //Interval Fires and drains buffer + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(5).Ticks); results.Messages.Count.Should().Be(3, "There should be 3 messages"); } [Fact] - public void PublishesOnIntervalEvent() + public void PublishesOnTimerCompletion() { - var intervalTimer = Observable.Interval(TimeSpan.FromMilliseconds(5), _scheduler).Select(_ => Unit.Default); + var intervalTimer = Observable.Timer(TimeSpan.FromMilliseconds(5), _scheduler).Select(_ => Unit.Default); var results = _source.Connect().BatchIf(_pausingSubject, true, intervalTimer, _scheduler).AsAggregator(); //Buffering @@ -202,27 +190,42 @@ public void PublishesOnIntervalEvent() _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1).Ticks); results.Messages.Count.Should().Be(0, "There should be 0 messages"); - //Interval Fires and drains buffer + //Timer should event, buffered items delivered _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(5).Ticks); results.Messages.Count.Should().Be(1, "There should be 1 messages"); - //Buffering again + //Unbuffered from here _source.AddOrUpdate(new Person("B", 2)); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1).Ticks); - results.Messages.Count.Should().Be(1, "There should be 1 messages"); - - //Interval Fires and drains buffer - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(5).Ticks); results.Messages.Count.Should().Be(2, "There should be 2 messages"); - //Buffering again + //Unbuffered from here _source.AddOrUpdate(new Person("C", 3)); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1).Ticks); - results.Messages.Count.Should().Be(2, "There should be 2 messages"); - - //Interval Fires and drains buffer - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(5).Ticks); results.Messages.Count.Should().Be(3, "There should be 3 messages"); } + + [Fact] + public void ResultsWillBeReceivedIfNotPaused() + { + _source.AddOrUpdate(new Person("A", 1)); + + //go forward an arbitary amount of time + _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); + _results.Messages.Count.Should().Be(1, "Should be 1 update"); + } + + [Fact] + public void WillApplyTimeout() + { + _pausingSubject.OnNext(true); + + //should timeout + _scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); + + _source.AddOrUpdate(new Person("A", 1)); + + _results.Messages.Count.Should().Be(1, "There should be 1 messages"); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/BufferInitialFixture.cs b/src/DynamicData.Tests/Cache/BufferInitialFixture.cs index 2285e80be..be2774244 100644 --- a/src/DynamicData.Tests/Cache/BufferInitialFixture.cs +++ b/src/DynamicData.Tests/Cache/BufferInitialFixture.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache @@ -17,27 +21,25 @@ public void BufferInitial() { var scheduler = new TestScheduler(); - using (var cache = new SourceCache(i => i.Name)) - using (var aggregator = cache.Connect().BufferInitial(TimeSpan.FromSeconds(1), scheduler).AsAggregator()) + using var cache = new SourceCache(i => i.Name); + using var aggregator = cache.Connect().BufferInitial(TimeSpan.FromSeconds(1), scheduler).AsAggregator(); + foreach (var item in People) { - foreach (var item in People) - { - cache.AddOrUpdate(item); - } + cache.AddOrUpdate(item); + } - aggregator.Data.Count.Should().Be(0); - aggregator.Messages.Count.Should().Be(0); + aggregator.Data.Count.Should().Be(0); + aggregator.Messages.Count.Should().Be(0); - scheduler.Start(); + scheduler.Start(); - aggregator.Data.Count.Should().Be(10_000); - aggregator.Messages.Count.Should().Be(1); + aggregator.Data.Count.Should().Be(10_000); + aggregator.Messages.Count.Should().Be(1); - cache.AddOrUpdate(new Person("_New",1)); + cache.AddOrUpdate(new Person("_New", 1)); - aggregator.Data.Count.Should().Be(10_001); - aggregator.Messages.Count.Should().Be(2); - } + aggregator.Data.Count.Should().Be(10_001); + aggregator.Messages.Count.Should().Be(2); } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ChangesReducerFixture.cs b/src/DynamicData.Tests/Cache/ChangesReducerFixture.cs index f27715fc6..7b78ab17e 100644 --- a/src/DynamicData.Tests/Cache/ChangesReducerFixture.cs +++ b/src/DynamicData.Tests/Cache/ChangesReducerFixture.cs @@ -1,41 +1,68 @@ using System.Collections.Generic; using System.Linq; + using DynamicData.Cache.Internal; using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { public class ChangesReducerFixture { - private static Person _testEntity = new Person("test", "test", 32); - private static int _testIndex = 0; - private static Change[] _changes = new[] + private static readonly Person _testEntity = new Person("test", "test", 32); + + private static readonly int _testIndex = 0; + + private static readonly Change[] _changes = new[] + { + new Change(ChangeReason.Add, _testEntity.Key, _testEntity, _testIndex), + new Change(ChangeReason.Remove, _testEntity.Key, _testEntity, _testIndex), + new Change(ChangeReason.Moved, _testEntity.Key, _testEntity, _testEntity, _testIndex, _testIndex + 1), + new Change(ChangeReason.Update, _testEntity.Key, _testEntity, _testEntity, _testIndex), + new Change(ChangeReason.Refresh, _testEntity.Key, _testEntity, _testIndex) + }; + + // ReSharper disable once MemberCanBePrivate.Global + public static IEnumerable ConstrainFirstValue(ChangeReason constraint, ChangeReason[] othersExcept) { - new Change(ChangeReason.Add, _testEntity.Key, _testEntity, _testIndex), - new Change(ChangeReason.Remove, _testEntity.Key, _testEntity, _testIndex), - new Change(ChangeReason.Moved, _testEntity.Key, _testEntity, _testEntity, _testIndex, - _testIndex + 1), - new Change(ChangeReason.Update, _testEntity.Key, _testEntity, _testEntity, _testIndex), - new Change(ChangeReason.Refresh, _testEntity.Key, _testEntity, _testIndex) - }; + var constrainedValue = _changes.Single(c => c.Reason == constraint); + var others = _changes.Where(c => c.Reason != constraint && !othersExcept.Contains(c.Reason)); + return others.Select(other => new object[] { constrainedValue, other }); + } - [Theory] - [MemberData(nameof(ConstrainFirstValue), new object[] {ChangeReason.Refresh, new ChangeReason[] {}})] - public void RefreshIsBeingOverridenByAnything(Change refresh, Change other) + // ReSharper disable once MemberCanBePrivate.Global + public static IEnumerable GetChanges() { - var result = ChangesReducer.Reduce(refresh, other); - result.Value.Should().Be(other); + return _changes.Select(x => new object[] { x }); } - [Theory] - [MemberData(nameof(ConstrainFirstValue), new object[] {ChangeReason.Remove, new[] { ChangeReason.Add}})] - public void RemoveOverridesAnythingButAdd(Change remove, Change other) + [Fact] + public void AddAndRemoveProduceNothing() { - var result = ChangesReducer.Reduce(other, remove); - result.Value.Should().Be(remove); + var add = _changes.Single(c => c.Reason == ChangeReason.Add); + var remove = _changes.Single(c => c.Reason == ChangeReason.Remove); + + var result = ChangesReducer.Reduce(add, remove); + result.HasValue.Should().Be(false); + } + + [Fact] + public void AddAndUpdateProduceAdd() + { + var updatedEntity = new Person(_testEntity.Key, 55); + var newIndex = _testIndex + 1; + + var add = new Change(ChangeReason.Add, _testEntity.Key, _testEntity, _testIndex); + var update = new Change(ChangeReason.Update, _testEntity.Key, updatedEntity, add.Current, newIndex, _testIndex); + + var result = ChangesReducer.Reduce(add, update); + var expected = new Change(ChangeReason.Add, _testEntity.Key, updatedEntity, newIndex); + + result.Value.Should().Be(expected); } [Theory] @@ -46,14 +73,12 @@ public void NoneGetsOverridenByAnything(Change c) result.Value.Should().Be(c); } - [Fact] - public void AddAndRemoveProduceNothing() + [Theory] + [MemberData(nameof(ConstrainFirstValue), ChangeReason.Refresh, new ChangeReason[] { })] + public void RefreshIsBeingOverridenByAnything(Change refresh, Change other) { - var add = _changes.Single(c => c.Reason == ChangeReason.Add); - var remove = _changes.Single(c => c.Reason == ChangeReason.Remove); - - var result = ChangesReducer.Reduce(add, remove); - result.HasValue.Should().Be(false); + var result = ChangesReducer.Reduce(refresh, other); + result.Value.Should().Be(other); } [Fact] @@ -68,19 +93,12 @@ public void RemoveAndAddProduceUpdate() result.Value.Should().Be(expected); } - [Fact] - public void AddAndUpdateProduceAdd() + [Theory] + [MemberData(nameof(ConstrainFirstValue), ChangeReason.Remove, new[] { ChangeReason.Add })] + public void RemoveOverridesAnythingButAdd(Change remove, Change other) { - var updatedEntity = new Person(_testEntity.Key, 55); - var newIndex = _testIndex + 1; - - var add = new Change(ChangeReason.Add, _testEntity.Key, _testEntity, _testIndex); - var update = new Change(ChangeReason.Update, _testEntity.Key, updatedEntity, add.Current, newIndex, _testIndex); - - var result = ChangesReducer.Reduce(add, update); - var expected = new Change(ChangeReason.Add, _testEntity.Key, updatedEntity, newIndex); - - result.Value.Should().Be(expected); + var result = ChangesReducer.Reduce(other, remove); + result.Value.Should().Be(remove); } [Fact] @@ -96,24 +114,9 @@ public void TwoUpdatesProduceUpdate() var secondUpdate = new Change(ChangeReason.Update, _testEntity.Key, updatedEntityTwo, updatedEntityOne, newIndexTwo, newIndexOne); var result = ChangesReducer.Reduce(firstUpdate, secondUpdate); - var expected = new Change(ChangeReason.Update, _testEntity.Key, updatedEntityTwo, _testEntity, newIndexTwo, _testIndex) ; + var expected = new Change(ChangeReason.Update, _testEntity.Key, updatedEntityTwo, _testEntity, newIndexTwo, _testIndex); result.Value.Should().Be(expected); } - - // ReSharper disable once MemberCanBePrivate.Global - public static IEnumerable ConstrainFirstValue(ChangeReason constraint, ChangeReason[] othersExcept) - { - var constrainedValue = _changes.Single(c => c.Reason == constraint); - var others = _changes.Where(c => c.Reason != constraint && !othersExcept.Contains(c.Reason)); - return others.Select(other => - new object[] {constrainedValue, other}); - } - - // ReSharper disable once MemberCanBePrivate.Global - public static IEnumerable GetChanges() - { - return _changes.Select(x=> new object[] { x }); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/DeferUntilLoadedFixture.cs b/src/DynamicData.Tests/Cache/DeferUntilLoadedFixture.cs index 47a6d2071..4a0e7ae4d 100644 --- a/src/DynamicData.Tests/Cache/DeferUntilLoadedFixture.cs +++ b/src/DynamicData.Tests/Cache/DeferUntilLoadedFixture.cs @@ -1,34 +1,42 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class DeferAnsdSkipFixture { [Fact] public void DeferUntilLoadedDoesNothingUntilDataHasBeenReceived() { bool updateReceived = false; - IChangeSet result = null; + IChangeSet? result = null; var cache = new SourceCache(p => p.Name); - var deferStream = cache.Connect().DeferUntilLoaded() - .Subscribe(changes => - { - updateReceived = true; - result = changes; - }); + var deferStream = cache.Connect().DeferUntilLoaded().Subscribe( + changes => + { + updateReceived = true; + result = changes; + }); var person = new Person("Test", 1); updateReceived.Should().BeFalse(); cache.AddOrUpdate(person); updateReceived.Should().BeTrue(); + + if (result is null) + { + throw new InvalidOperationException(nameof(result)); + } + result.Adds.Should().Be(1); result.First().Current.Should().Be(person); deferStream.Dispose(); @@ -41,8 +49,7 @@ public void SkipInitialDoesNotReturnTheFirstBatchOfData() var cache = new SourceCache(p => p.Name); - var deferStream = cache.Connect().SkipInitial() - .Subscribe(changes => updateReceived = true); + var deferStream = cache.Connect().SkipInitial().Subscribe(changes => updateReceived = true); updateReceived.Should().BeFalse(); @@ -55,4 +62,4 @@ public void SkipInitialDoesNotReturnTheFirstBatchOfData() deferStream.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/DisposeManyFixture.cs b/src/DynamicData.Tests/Cache/DisposeManyFixture.cs index bd31b2f70..ec4800e10 100644 --- a/src/DynamicData.Tests/Cache/DisposeManyFixture.cs +++ b/src/DynamicData.Tests/Cache/DisposeManyFixture.cs @@ -1,31 +1,17 @@ using System; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class DisposeManyFixture: IDisposable + public class DisposeManyFixture : IDisposable { - private class DisposableObject : IDisposable - { - public bool IsDisposed { get; private set; } - public int Id { get; private set; } - - public DisposableObject(int id) - { - Id = id; - } - - public void Dispose() - { - IsDisposed = true; - } - } + private readonly ChangeSetAggregator _results; private readonly ISourceCache _source; - private readonly ChangeSetAggregator _results; public DisposeManyFixture() { @@ -33,6 +19,16 @@ public DisposeManyFixture() _results = new ChangeSetAggregator(_source.Connect().DisposeMany()); } + [Fact] + public void AddWillNotCallDispose() + { + _source.AddOrUpdate(new DisposableObject(1)); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Items.First().IsDisposed.Should().Be(false, "Should not be disposed"); + } + public void Dispose() { _source.Dispose(); @@ -40,13 +36,13 @@ public void Dispose() } [Fact] - public void AddWillNotCallDispose() + public void EverythingIsDisposedWhenStreamIsDisposed() { - _source.AddOrUpdate(new DisposableObject(1)); + _source.AddOrUpdate(Enumerable.Range(1, 10).Select(i => new DisposableObject(i))); + _source.Clear(); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - _results.Data.Items.First().IsDisposed.Should().Be(false, "Should not be disposed"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[1].All(d => d.Current.IsDisposed).Should().BeTrue(); } [Fact] @@ -72,14 +68,21 @@ public void UpdateWillCallDispose() _results.Messages[1].First().Previous.Value.IsDisposed.Should().Be(true, "Previous should be disposed"); } - [Fact] - public void EverythingIsDisposedWhenStreamIsDisposed() + private class DisposableObject : IDisposable { - _source.AddOrUpdate(Enumerable.Range(1, 10).Select(i => new DisposableObject(i))); - _source.Clear(); + public DisposableObject(int id) + { + Id = id; + } - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[1].All(d => d.Current.IsDisposed).Should().BeTrue(); + public int Id { get; private set; } + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/DistinctFixture.cs b/src/DynamicData.Tests/Cache/DistinctFixture.cs index c2639f110..38a453f5a 100644 --- a/src/DynamicData.Tests/Cache/DistinctFixture.cs +++ b/src/DynamicData.Tests/Cache/DistinctFixture.cs @@ -1,107 +1,72 @@ - -using System; +using System; using System.Linq; using System.Reactive.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class DistinctFixture: IDisposable + public class DistinctFixture : IDisposable { - private readonly ISourceCache _source; private readonly DistinctChangeSetAggregator _results; - public DistinctFixture() + private readonly ISourceCache _source; + + public DistinctFixture() { _source = new SourceCache(p => p.Name); _results = _source.Connect().DistinctValues(p => p.Age).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] - public void FiresAddWhenaNewItemIsAdded() + public void BreakWithLoadsOfUpdates() { - _source.AddOrUpdate(new Person("Person1", 20)); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person("Person2", 12)); + updater.AddOrUpdate(new Person("Person1", 1)); + updater.AddOrUpdate(new Person("Person1", 1)); + updater.AddOrUpdate(new Person("Person2", 12)); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - _results.Data.Items.First().Should().Be(20, "Should 20"); - } + updater.AddOrUpdate(new Person("Person3", 13)); + updater.AddOrUpdate(new Person("Person4", 14)); + }); - [Fact] - public void FiresBatchResultOnce() - { - _source.Edit(updater => - { - updater.AddOrUpdate(new Person("Person1", 20)); - updater.AddOrUpdate(new Person("Person2", 21)); - updater.AddOrUpdate(new Person("Person3", 22)); - }); + _results.Data.Items.Should().BeEquivalentTo(new[] { 1, 12, 13, 14 }); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(3, "Should be 3 items in the cache"); + //This previously threw + _source.Remove(new Person("Person3", 13)); - _results.Data.Items.Should().BeEquivalentTo(new[] {20, 21, 22}); - _results.Data.Items.First().Should().Be(20, "Should 20"); + _results.Data.Items.Should().BeEquivalentTo(new[] { 1, 12, 14 }); + } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); } [Fact] public void DuplicatedResultsResultInNoAdditionalMessage() { - _source.Edit(updater => - { - updater.AddOrUpdate(new Person("Person1", 20)); - updater.AddOrUpdate(new Person("Person1", 20)); - updater.AddOrUpdate(new Person("Person1", 20)); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person("Person1", 20)); + updater.AddOrUpdate(new Person("Person1", 20)); + updater.AddOrUpdate(new Person("Person1", 20)); + }); _results.Messages.Count.Should().Be(1, "Should be 1 update message"); _results.Data.Count.Should().Be(1, "Should be 1 items in the cache"); _results.Data.Items.First().Should().Be(20, "Should 20"); } - [Fact] - public void RemovingAnItemRemovesTheDistinct() - { - _source.AddOrUpdate(new Person("Person1", 20)); - _source.Remove(new Person("Person1", 20)); - _results.Messages.Count.Should().Be(2, "Should be 1 update message"); - _results.Data.Count.Should().Be(0, "Should be 1 items in the cache"); - - _results.Messages.First().Adds.Should().Be(1, "First message should be an add"); - _results.Messages.Skip(1).First().Removes.Should().Be(1, "Second messsage should be a remove"); - } - - [Fact] - public void BreakWithLoadsOfUpdates() - { - _source.Edit(updater => - { - updater.AddOrUpdate(new Person("Person2", 12)); - updater.AddOrUpdate(new Person("Person1", 1)); - updater.AddOrUpdate(new Person("Person1", 1)); - updater.AddOrUpdate(new Person("Person2", 12)); - - updater.AddOrUpdate(new Person("Person3", 13)); - updater.AddOrUpdate(new Person("Person4", 14)); - }); - - _results.Data.Items.Should().BeEquivalentTo(new[] {1, 12, 13, 14}); - - //This previously threw - _source.Remove(new Person("Person3", 13)); - - _results.Data.Items.Should().BeEquivalentTo(new[] {1, 12, 14}); - } - [Fact] public void DuplicateKeysRefreshAfterRemove() { @@ -118,12 +83,52 @@ public void DuplicateKeysRefreshAfterRemove() source1.Refresh(person); // would previously throw KeyNotFoundException here results.Messages.Should().HaveCount(1); - results.Data.Items.Should().BeEquivalentTo(new[] {12}); + results.Data.Items.Should().BeEquivalentTo(new[] { 12 }); source1.Remove(person); results.Messages.Should().HaveCount(2); results.Data.Items.Should().BeEmpty(); } + + [Fact] + public void FiresAddWhenaNewItemIsAdded() + { + _source.AddOrUpdate(new Person("Person1", 20)); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Items.First().Should().Be(20, "Should 20"); + } + + [Fact] + public void FiresBatchResultOnce() + { + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person("Person1", 20)); + updater.AddOrUpdate(new Person("Person2", 21)); + updater.AddOrUpdate(new Person("Person3", 22)); + }); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(3, "Should be 3 items in the cache"); + + _results.Data.Items.Should().BeEquivalentTo(new[] { 20, 21, 22 }); + _results.Data.Items.First().Should().Be(20, "Should 20"); + } + + [Fact] + public void RemovingAnItemRemovesTheDistinct() + { + _source.AddOrUpdate(new Person("Person1", 20)); + _source.Remove(new Person("Person1", 20)); + _results.Messages.Count.Should().Be(2, "Should be 1 update message"); + _results.Data.Count.Should().Be(0, "Should be 1 items in the cache"); + + _results.Messages.First().Adds.Should().Be(1, "First message should be an add"); + _results.Messages.Skip(1).First().Removes.Should().Be(1, "Second messsage should be a remove"); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/DynamicAndFixture.cs b/src/DynamicData.Tests/Cache/DynamicAndFixture.cs index 6b1a35294..f5f0fae70 100644 --- a/src/DynamicData.Tests/Cache/DynamicAndFixture.cs +++ b/src/DynamicData.Tests/Cache/DynamicAndFixture.cs @@ -1,23 +1,29 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class DynamicAndFixture: IDisposable + public class DynamicAndFixture : IDisposable { - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + private readonly RandomPersonGenerator _generator = new(); + + private readonly ChangeSetAggregator _results; + + private readonly ISourceList>> _source; private readonly ISourceCache _source1; + private readonly ISourceCache _source2; + private readonly ISourceCache _source3; - private readonly ISourceList>> _source; - private readonly ChangeSetAggregator _results; - public DynamicAndFixture() + public DynamicAndFixture() { _source1 = new SourceCache(p => p.Name); _source2 = new SourceCache(p => p.Name); @@ -26,6 +32,26 @@ public DynamicAndFixture() _results = _source.And().AsAggregator(); } + [Fact] + public void AddAndRemoveLists() + { + var items = _generator.Take(100).ToArray(); + _source1.AddOrUpdate(items.Take(20)); + _source2.AddOrUpdate(items.Skip(10).Take(10)); + + _source.Add(_source1.Connect()); + _source.Add(_source2.Connect()); + + _results.Data.Count.Should().Be(10); + _results.Data.Items.Should().BeEquivalentTo(items.Skip(10).Take(10)); + + _source.Add(_source3.Connect()); + _results.Data.Count.Should().Be(0); + + _source.RemoveAt(2); + _results.Data.Count.Should().Be(10); + } + public void Dispose() { _source1.Dispose(); @@ -36,20 +62,28 @@ public void Dispose() } [Fact] - public void UpdatingOneSourceOnlyProducesNoResults() + public void RemoveAllLists() { + var items = _generator.Take(100).ToArray(); + + _source1.AddOrUpdate(items.Take(10)); + _source2.AddOrUpdate(items.Skip(20).Take(10)); + _source3.AddOrUpdate(items.Skip(30)); + _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); + _source.Add(_source3.Connect()); - var person = new Person("Adult1", 50); - _source1.AddOrUpdate(person); + _source.RemoveAt(2); + _source.RemoveAt(1); + _source.RemoveAt(0); + //s _source.Clear(); - _results.Messages.Count.Should().Be(0, "Should have no updates"); - _results.Data.Count.Should().Be(0, "Cache should have no items"); + _results.Data.Count.Should().Be(0); } [Fact] - public void UpdatingBothProducesResults() + public void RemovingFromOneRemovesFromResult() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); @@ -57,13 +91,14 @@ public void UpdatingBothProducesResults() var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - _results.Messages.Count.Should().Be(1, "Should have no updates"); - _results.Data.Count.Should().Be(1, "Cache should have no items"); - _results.Data.Items.First().Should().Be(person, "Should be same person"); + + _source2.Remove(person); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Data.Count.Should().Be(0, "Cache should have no items"); } [Fact] - public void RemovingFromOneRemovesFromResult() + public void UpdatingBothProducesResults() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); @@ -71,10 +106,9 @@ public void RemovingFromOneRemovesFromResult() var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - - _source2.Remove(person); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Data.Count.Should().Be(0, "Cache should have no items"); + _results.Messages.Count.Should().Be(1, "Should have no updates"); + _results.Data.Count.Should().Be(1, "Cache should have no items"); + _results.Data.Items.First().Should().Be(person, "Should be same person"); } [Fact] @@ -95,44 +129,16 @@ public void UpdatingOneProducesOnlyOneUpdate() } [Fact] - public void AddAndRemoveLists() - { - var items = _generator.Take(100).ToArray(); - _source1.AddOrUpdate(items.Take(20)); - _source2.AddOrUpdate(items.Skip(10).Take(10)); - - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - - _results.Data.Count.Should().Be(10); - _results.Data.Items.Should().BeEquivalentTo(items.Skip(10).Take(10)); - - _source.Add(_source3.Connect()); - _results.Data.Count.Should().Be(0); - - _source.RemoveAt(2); - _results.Data.Count.Should().Be(10); - } - - [Fact] - public void RemoveAllLists() + public void UpdatingOneSourceOnlyProducesNoResults() { - var items = _generator.Take(100).ToArray(); - - _source1.AddOrUpdate(items.Take(10)); - _source2.AddOrUpdate(items.Skip(20).Take(10)); - _source3.AddOrUpdate(items.Skip(30)); - _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - _source.RemoveAt(2); - _source.RemoveAt(1); - _source.RemoveAt(0); - //s _source.Clear(); + var person = new Person("Adult1", 50); + _source1.AddOrUpdate(person); - _results.Data.Count.Should().Be(0); + _results.Messages.Count.Should().Be(0, "Should have no updates"); + _results.Data.Count.Should().Be(0, "Cache should have no items"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/DynamicExceptFixture.cs b/src/DynamicData.Tests/Cache/DynamicExceptFixture.cs index 16920ed48..4f61331fd 100644 --- a/src/DynamicData.Tests/Cache/DynamicExceptFixture.cs +++ b/src/DynamicData.Tests/Cache/DynamicExceptFixture.cs @@ -1,23 +1,29 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class DynamicExceptFixture: IDisposable + public class DynamicExceptFixture : IDisposable { - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + private readonly RandomPersonGenerator _generator = new(); + + private readonly ChangeSetAggregator _results; + + private readonly ISourceList>> _source; private readonly ISourceCache _source1; + private readonly ISourceCache _source2; + private readonly ISourceCache _source3; - private readonly ISourceList>> _source; - private readonly ChangeSetAggregator _results; - public DynamicExceptFixture() + public DynamicExceptFixture() { _source1 = new SourceCache(p => p.Name); _source2 = new SourceCache(p => p.Name); @@ -26,6 +32,31 @@ public DynamicExceptFixture() _results = _source.Except().AsAggregator(); } + [Fact] + public void AddAndRemoveLists() + { + var items = _generator.Take(100).OrderBy(p => p.Name).ToArray(); + + _source1.AddOrUpdate(items); + _source2.AddOrUpdate(items.Take(10)); + _source3.AddOrUpdate(items.Skip(90).Take(10)); + + _source.Add(_source1.Connect()); + _source.Add(_source2.Connect()); + _source.Add(_source3.Connect()); + + _results.Data.Count.Should().Be(80); + _results.Data.Items.Should().BeEquivalentTo(items.Skip(10).Take(80)); + + _source.RemoveAt(2); + _results.Data.Count.Should().Be(90); + _results.Data.Items.Should().BeEquivalentTo(items.Skip(10)); + + _source.RemoveAt(0); + _results.Data.Count.Should().Be(10); + _results.Data.Items.Should().BeEquivalentTo(items.Take(10)); + } + public void Dispose() { _source1.Dispose(); @@ -36,30 +67,35 @@ public void Dispose() } [Fact] - public void UpdatingOneSourceOnlyProducesResult() + public void DoNotIncludeExceptListItems() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); var person = new Person("Adult1", 50); + _source2.AddOrUpdate(person); _source1.AddOrUpdate(person); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Messages.Count.Should().Be(0, "Should have no updates"); + _results.Data.Count.Should().Be(0, "Cache should have no items"); } [Fact] - public void DoNotIncludeExceptListItems() + public void RemoveAllLists() { + var items = _generator.Take(100).ToArray(); + + _source1.AddOrUpdate(items.Take(10)); + _source2.AddOrUpdate(items.Skip(10).Take(10)); + _source3.AddOrUpdate(items.Skip(20)); + _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); + _source.Add(_source3.Connect()); - var person = new Person("Adult1", 50); - _source2.AddOrUpdate(person); - _source1.AddOrUpdate(person); + _source.Clear(); - _results.Messages.Count.Should().Be(0, "Should have no updates"); - _results.Data.Count.Should().Be(0, "Cache should have no items"); + _results.Data.Count.Should().Be(0); } [Fact] @@ -78,46 +114,16 @@ public void RemovedAnItemFromExceptThenIncludesTheItem() } [Fact] - public void AddAndRemoveLists() - { - var items = _generator.Take(100).OrderBy(p => p.Name).ToArray(); - - _source1.AddOrUpdate(items); - _source2.AddOrUpdate(items.Take(10)); - _source3.AddOrUpdate(items.Skip(90).Take(10)); - - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - - _results.Data.Count.Should().Be(80); - _results.Data.Items.Should().BeEquivalentTo(items.Skip(10).Take(80)); - - _source.RemoveAt(2); - _results.Data.Count.Should().Be(90); - _results.Data.Items.Should().BeEquivalentTo(items.Skip(10)); - - _source.RemoveAt(0); - _results.Data.Count.Should().Be(10); - _results.Data.Items.Should().BeEquivalentTo(items.Take(10)); - } - - [Fact] - public void RemoveAllLists() + public void UpdatingOneSourceOnlyProducesResult() { - var items = _generator.Take(100).ToArray(); - - _source1.AddOrUpdate(items.Take(10)); - _source2.AddOrUpdate(items.Skip(10).Take(10)); - _source3.AddOrUpdate(items.Skip(20)); - _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - _source.Clear(); + var person = new Person("Adult1", 50); + _source1.AddOrUpdate(person); - _results.Data.Count.Should().Be(0); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/DynamicOrFixture.cs b/src/DynamicData.Tests/Cache/DynamicOrFixture.cs index 858912532..a2916d0b4 100644 --- a/src/DynamicData.Tests/Cache/DynamicOrFixture.cs +++ b/src/DynamicData.Tests/Cache/DynamicOrFixture.cs @@ -1,23 +1,29 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class DynamicOrFixture: IDisposable + public class DynamicOrFixture : IDisposable { - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + private readonly RandomPersonGenerator _generator = new(); + + private readonly ChangeSetAggregator _results; + + private readonly ISourceList>> _source; private readonly ISourceCache _source1; + private readonly ISourceCache _source2; + private readonly ISourceCache _source3; - private readonly ISourceList>> _source; - private readonly ChangeSetAggregator _results; - public DynamicOrFixture() + public DynamicOrFixture() { _source1 = new SourceCache(p => p.Name); _source2 = new SourceCache(p => p.Name); @@ -26,6 +32,29 @@ public DynamicOrFixture() _results = _source.Or().AsAggregator(); } + [Fact] + public void AddAndRemoveLists() + { + var items = _generator.Take(100).ToArray(); + + _source1.AddOrUpdate(items.Take(10)); + _source2.AddOrUpdate(items.Skip(10).Take(10)); + _source3.AddOrUpdate(items.Skip(20)); + + _source.Add(_source1.Connect()); + _source.Add(_source2.Connect()); + _source.Add(_source3.Connect()); + + _results.Data.Count.Should().Be(100); + _results.Data.Items.Should().BeEquivalentTo(items); + + _source.RemoveAt(1); + var result = items.Take(10).Union(items.Skip(20)); + + _results.Data.Count.Should().Be(90); + _results.Data.Items.Should().BeEquivalentTo(result); + } + public void Dispose() { _source1.Dispose(); @@ -36,20 +65,25 @@ public void Dispose() } [Fact] - public void UpdatingOneSourceOnlyProducesResult() + public void RemoveAllLists() { + var items = _generator.Take(100).ToArray(); + + _source1.AddOrUpdate(items.Take(10)); + _source2.AddOrUpdate(items.Skip(10).Take(10)); + _source3.AddOrUpdate(items.Skip(20)); + _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); + _source.Add(_source3.Connect()); - var person = new Person("Adult1", 50); - _source1.AddOrUpdate(person); + _source.Clear(); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Count.Should().Be(0); } [Fact] - public void UpdatingBothProducesResultsAndDoesNotDuplicateTheMessage() + public void RemovingFromOneDoesNotFromResult() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); @@ -57,13 +91,14 @@ public void UpdatingBothProducesResultsAndDoesNotDuplicateTheMessage() var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - _results.Messages.Count.Should().Be(1, "Should have no updates"); + + _source2.Remove(person); + _results.Messages.Count.Should().Be(1, "Should be 2 updates"); _results.Data.Count.Should().Be(1, "Cache should have no items"); - _results.Data.Items.First().Should().Be(person, "Should be same person"); } [Fact] - public void RemovingFromOneDoesNotFromResult() + public void UpdatingBothProducesResultsAndDoesNotDuplicateTheMessage() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); @@ -71,10 +106,9 @@ public void RemovingFromOneDoesNotFromResult() var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - - _source2.Remove(person); - _results.Messages.Count.Should().Be(1, "Should be 2 updates"); + _results.Messages.Count.Should().Be(1, "Should have no updates"); _results.Data.Count.Should().Be(1, "Cache should have no items"); + _results.Data.Items.First().Should().Be(person, "Should be same person"); } [Fact] @@ -95,45 +129,16 @@ public void UpdatingOneProducesOnlyOneUpdate() } [Fact] - public void AddAndRemoveLists() - { - var items = _generator.Take(100).ToArray(); - - _source1.AddOrUpdate(items.Take(10)); - _source2.AddOrUpdate(items.Skip(10).Take(10)); - _source3.AddOrUpdate(items.Skip(20)); - - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - - _results.Data.Count.Should().Be(100); - _results.Data.Items.Should().BeEquivalentTo(items); - - _source.RemoveAt(1); - var result = items.Take(10) - .Union(items.Skip(20)); - - _results.Data.Count.Should().Be(90); - _results.Data.Items.Should().BeEquivalentTo(result); - } - - [Fact] - public void RemoveAllLists() + public void UpdatingOneSourceOnlyProducesResult() { - var items = _generator.Take(100).ToArray(); - - _source1.AddOrUpdate(items.Take(10)); - _source2.AddOrUpdate(items.Skip(10).Take(10)); - _source3.AddOrUpdate(items.Skip(20)); - _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - _source.Clear(); + var person = new Person("Adult1", 50); + _source1.AddOrUpdate(person); - _results.Data.Count.Should().Be(0); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/DynamicXorFixture.cs b/src/DynamicData.Tests/Cache/DynamicXorFixture.cs index f2cd3d7f6..18db47cc0 100644 --- a/src/DynamicData.Tests/Cache/DynamicXorFixture.cs +++ b/src/DynamicData.Tests/Cache/DynamicXorFixture.cs @@ -1,22 +1,27 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class DynamicXorFixture: IDisposable + public class DynamicXorFixture : IDisposable { - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + private readonly RandomPersonGenerator _generator = new(); + + private readonly ChangeSetAggregator _results; + + private readonly ISourceList>> _source; private readonly ISourceCache _source1; + private readonly ISourceCache _source2; - private readonly ISourceCache _source3; - private readonly ISourceList>> _source; - private readonly ChangeSetAggregator _results; + private readonly ISourceCache _source3; public DynamicXorFixture() { @@ -27,6 +32,29 @@ public DynamicXorFixture() _results = _source.Xor().AsAggregator(); } + [Fact] + public void AddAndRemoveLists() + { + var items = _generator.Take(100).ToArray(); + + _source1.AddOrUpdate(items.Take(10)); + _source2.AddOrUpdate(items.Skip(10).Take(10)); + _source3.AddOrUpdate(items.Skip(20)); + + _source.Add(_source1.Connect()); + _source.Add(_source2.Connect()); + _source.Add(_source3.Connect()); + + _results.Data.Count.Should().Be(100); + _results.Data.Items.Should().BeEquivalentTo(items); + + _source.RemoveAt(1); + + var result = items.Take(10).Union(items.Skip(20)); + _results.Data.Count.Should().Be(90); + _results.Data.Items.Should().BeEquivalentTo(result); + } + public void Dispose() { _source1.Dispose(); @@ -37,20 +65,25 @@ public void Dispose() } [Fact] - public void UpdatingOneSourceOnlyProducesResult() + public void RemoveAllLists() { + var items = _generator.Take(100).ToArray(); + + _source1.AddOrUpdate(items.Take(10)); + _source2.AddOrUpdate(items.Skip(10).Take(10)); + _source3.AddOrUpdate(items.Skip(20)); + _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); + _source.Add(_source3.Connect()); - var person = new Person("Adult1", 50); - _source1.AddOrUpdate(person); + _source.Clear(); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Count.Should().Be(0); } [Fact] - public void UpdatingBothDoeNotProducesResult() + public void RemovingFromOneDoesNotFromResult() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); @@ -58,11 +91,14 @@ public void UpdatingBothDoeNotProducesResult() var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - _results.Data.Count.Should().Be(0, "Cache should have no items"); + + _source2.Remove(person); + _results.Messages.Count.Should().Be(3, "Should be 2 updates"); + _results.Data.Count.Should().Be(1, "Cache should have no items"); } [Fact] - public void RemovingFromOneDoesNotFromResult() + public void UpdatingBothDoeNotProducesResult() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); @@ -70,10 +106,7 @@ public void RemovingFromOneDoesNotFromResult() var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - - _source2.Remove(person); - _results.Messages.Count.Should().Be(3, "Should be 2 updates"); - _results.Data.Count.Should().Be(1, "Cache should have no items"); + _results.Data.Count.Should().Be(0, "Cache should have no items"); } [Fact] @@ -93,44 +126,16 @@ public void UpdatingOneProducesOnlyOneUpdate() } [Fact] - public void AddAndRemoveLists() - { - var items = _generator.Take(100).ToArray(); - - _source1.AddOrUpdate(items.Take(10)); - _source2.AddOrUpdate(items.Skip(10).Take(10)); - _source3.AddOrUpdate(items.Skip(20)); - - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - - _results.Data.Count.Should().Be(100); - _results.Data.Items.Should().BeEquivalentTo(items); - - _source.RemoveAt(1); - - var result = items.Take(10).Union(items.Skip(20)); - _results.Data.Count.Should().Be(90); - _results.Data.Items.Should().BeEquivalentTo(result); - } - - [Fact] - public void RemoveAllLists() + public void UpdatingOneSourceOnlyProducesResult() { - var items = _generator.Take(100).ToArray(); - - _source1.AddOrUpdate(items.Take(10)); - _source2.AddOrUpdate(items.Skip(10).Take(10)); - _source3.AddOrUpdate(items.Skip(20)); - _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - _source.Clear(); + var person = new Person("Adult1", 50); + _source1.AddOrUpdate(person); - _results.Data.Count.Should().Be(0); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/EditDiffFixture.cs b/src/DynamicData.Tests/Cache/EditDiffFixture.cs index 1cc186903..320787804 100644 --- a/src/DynamicData.Tests/Cache/EditDiffFixture.cs +++ b/src/DynamicData.Tests/Cache/EditDiffFixture.cs @@ -1,61 +1,48 @@ - -using System; +using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class EditDiffFixture: IDisposable + public class EditDiffFixture : IDisposable { private readonly SourceCache _cache; + private readonly ChangeSetAggregator _result; - public EditDiffFixture() + public EditDiffFixture() { _cache = new SourceCache(p => p.Name); _result = _cache.Connect().AsAggregator(); _cache.AddOrUpdate(Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray()); } - public void Dispose() - { - _cache.Dispose(); - _result.Dispose(); - } - [Fact] - public void New() + public void Amends() { - var newPeople = Enumerable.Range(1, 15).Select(i => new Person("Name" + i, i)).ToArray(); + var newList = Enumerable.Range(5, 3).Select(i => new Person("Name" + i, i + 10)).ToArray(); + _cache.EditDiff(newList, (current, previous) => Person.AgeComparer.Equals(current, previous)); - _cache.EditDiff(newPeople, (current, previous) => Person.AgeComparer.Equals(current, previous)); + _cache.Count.Should().Be(3); - _cache.Count.Should().Be(15); - _cache.Items.Should().BeEquivalentTo(newPeople); var lastChange = _result.Messages.Last(); - lastChange.Adds.Should().Be(5); - } - - [Fact] - public void EditWithSameData() - { - var newPeople = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); - - _cache.EditDiff(newPeople, (current, previous) => Person.AgeComparer.Equals(current, previous)); + lastChange.Adds.Should().Be(0); + lastChange.Updates.Should().Be(3); + lastChange.Removes.Should().Be(7); - _cache.Count.Should().Be(10); - _cache.Items.Should().BeEquivalentTo(newPeople); - _result.Messages.Count.Should().Be(1); + _cache.Items.Should().BeEquivalentTo(newList); } [Fact] - public void Amends() + public void Amends_WithEqualityComparer() { var newList = Enumerable.Range(5, 3).Select(i => new Person("Name" + i, i + 10)).ToArray(); - _cache.EditDiff(newList, (current, previous) => Person.AgeComparer.Equals(current, previous)); + _cache.EditDiff(newList, Person.AgeComparer); _cache.Count.Should().Be(3); @@ -67,45 +54,43 @@ public void Amends() _cache.Items.Should().BeEquivalentTo(newList); } - [Fact] - public void Removes() + public void Dispose() { - var newList = Enumerable.Range(1, 7).Select(i => new Person("Name" + i, i)).ToArray(); - _cache.EditDiff(newList, (current, previous) => Person.AgeComparer.Equals(current, previous)); + _cache.Dispose(); + _result.Dispose(); + } - _cache.Count.Should().Be(7); + [Fact] + public void EditWithSameData() + { + var newPeople = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); - var lastChange = _result.Messages.Last(); - lastChange.Adds.Should().Be(0); - lastChange.Updates.Should().Be(0); - lastChange.Removes.Should().Be(3); + _cache.EditDiff(newPeople, (current, previous) => Person.AgeComparer.Equals(current, previous)); - _cache.Items.Should().BeEquivalentTo(newList); + _cache.Count.Should().Be(10); + _cache.Items.Should().BeEquivalentTo(newPeople); + _result.Messages.Count.Should().Be(1); } [Fact] - public void VariousChanges() + public void EditWithSameData_WithEqualityComparer() { - var newList = Enumerable.Range(6, 10).Select(i => new Person("Name" + i, i + 10)).ToArray(); + var newPeople = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); - _cache.EditDiff(newList, (current, previous) => Person.AgeComparer.Equals(current, previous)); + _cache.EditDiff(newPeople, Person.AgeComparer); _cache.Count.Should().Be(10); - + _cache.Items.Should().BeEquivalentTo(newPeople); var lastChange = _result.Messages.Last(); - lastChange.Adds.Should().Be(5); - lastChange.Updates.Should().Be(5); - lastChange.Removes.Should().Be(5); - - _cache.Items.Should().BeEquivalentTo(newList); + _result.Messages.Count.Should().Be(1); } [Fact] - public void New_WithEqualityComparer() + public void New() { var newPeople = Enumerable.Range(1, 15).Select(i => new Person("Name" + i, i)).ToArray(); - _cache.EditDiff(newPeople, Person.AgeComparer); + _cache.EditDiff(newPeople, (current, previous) => Person.AgeComparer.Equals(current, previous)); _cache.Count.Should().Be(15); _cache.Items.Should().BeEquivalentTo(newPeople); @@ -114,30 +99,30 @@ public void New_WithEqualityComparer() } [Fact] - public void EditWithSameData_WithEqualityComparer() + public void New_WithEqualityComparer() { - var newPeople = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); + var newPeople = Enumerable.Range(1, 15).Select(i => new Person("Name" + i, i)).ToArray(); _cache.EditDiff(newPeople, Person.AgeComparer); - _cache.Count.Should().Be(10); + _cache.Count.Should().Be(15); _cache.Items.Should().BeEquivalentTo(newPeople); var lastChange = _result.Messages.Last(); - _result.Messages.Count.Should().Be(1); + lastChange.Adds.Should().Be(5); } [Fact] - public void Amends_WithEqualityComparer() + public void Removes() { - var newList = Enumerable.Range(5, 3).Select(i => new Person("Name" + i, i + 10)).ToArray(); - _cache.EditDiff(newList, Person.AgeComparer); + var newList = Enumerable.Range(1, 7).Select(i => new Person("Name" + i, i)).ToArray(); + _cache.EditDiff(newList, (current, previous) => Person.AgeComparer.Equals(current, previous)); - _cache.Count.Should().Be(3); + _cache.Count.Should().Be(7); var lastChange = _result.Messages.Last(); lastChange.Adds.Should().Be(0); - lastChange.Updates.Should().Be(3); - lastChange.Removes.Should().Be(7); + lastChange.Updates.Should().Be(0); + lastChange.Removes.Should().Be(3); _cache.Items.Should().BeEquivalentTo(newList); } @@ -158,6 +143,23 @@ public void Removes_WithEqualityComparer() _cache.Items.Should().BeEquivalentTo(newList); } + [Fact] + public void VariousChanges() + { + var newList = Enumerable.Range(6, 10).Select(i => new Person("Name" + i, i + 10)).ToArray(); + + _cache.EditDiff(newList, (current, previous) => Person.AgeComparer.Equals(current, previous)); + + _cache.Count.Should().Be(10); + + var lastChange = _result.Messages.Last(); + lastChange.Adds.Should().Be(5); + lastChange.Updates.Should().Be(5); + lastChange.Removes.Should().Be(5); + + _cache.Items.Should().BeEquivalentTo(newList); + } + [Fact] public void VariousChanges_WithEqualityComparer() { @@ -175,4 +177,4 @@ public void VariousChanges_WithEqualityComparer() _cache.Items.Should().BeEquivalentTo(newList); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/EnumerableObservableToObservableChangeSetFixture.cs b/src/DynamicData.Tests/Cache/EnumerableObservableToObservableChangeSetFixture.cs index 4d5f57827..c64933acc 100644 --- a/src/DynamicData.Tests/Cache/EnumerableObservableToObservableChangeSetFixture.cs +++ b/src/DynamicData.Tests/Cache/EnumerableObservableToObservableChangeSetFixture.cs @@ -2,34 +2,58 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { - public class EnumerableObservableToObservableChangeSetFixture { [Fact] - public void OnNextProducesAnAddChangeForEnumerableSource() + public void ExpireAfterTime() { var subject = new Subject>(); - var results = subject.ToObservableChangeSet().AsAggregator(); + var scheduler = new TestScheduler(); + var results = subject.ToObservableChangeSet(t => TimeSpan.FromMinutes(1), scheduler).AsAggregator(); - var people = new[] - { - new Person("A", 1), - new Person("B", 2), - new Person("C", 3) - }; + var people = Enumerable.Range(1, 200).Select(i => new Person("p" + i.ToString("000"), i)).ToArray(); subject.OnNext(people); - results.Messages.Count.Should().Be(1, "Should be 1 updates"); - results.Data.Count.Should().Be(3, "Should be 1 item in the cache"); - results.Data.Items.Should().BeEquivalentTo(results.Data.Items, "Lists should be equivalent"); + scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); + //scheduler.Start(); + results.Messages.Count.Should().Be(2, "Should be 300 messages"); + results.Messages.Sum(x => x.Adds).Should().Be(200, "Should be 200 adds"); + results.Messages.Sum(x => x.Removes).Should().Be(200, "Should be 100 removes"); + results.Data.Count.Should().Be(0, "Should be no data in the cache"); + } + + [Fact] + public void LimitSizeTo() + { + var subject = new Subject>(); + var scheduler = new TestScheduler(); + var results = subject.ToObservableChangeSet(100, scheduler).AsAggregator(); + + var people = Enumerable.Range(1, 200).Select(i => new Person("p" + i.ToString("000"), i)).ToArray(); + + subject.OnNext(people); + + scheduler.AdvanceBy(1); + + results.Messages.Sum(x => x.Adds).Should().Be(200, "Should be 200 adds"); + results.Messages.Sum(x => x.Removes).Should().Be(100, "Should be 100 removes"); + results.Data.Count.Should().Be(100, "Should be 1 item in the cache"); + + var expected = people.Skip(100).ToArray().OrderBy(p => p.Name).ToArray(); + var actual = results.Data.Items.OrderBy(p => p.Name).ToArray(); + actual.Should().BeEquivalentTo(expected, "Only second hundred should be in the cache"); } [Fact] @@ -39,11 +63,11 @@ public void OnNextProducesAnAddAndRemoveChangeForEnumerableSource() var results = subject.ToObservableChangeSet(p => p.Name).AsAggregator(); var people = new[] - { - new Person("A", 1), - new Person("B", 2), - new Person("C", 3) - }; + { + new Person("A", 1), + new Person("B", 2), + new Person("C", 3) + }; subject.OnNext(people); @@ -51,10 +75,10 @@ public void OnNextProducesAnAddAndRemoveChangeForEnumerableSource() results.Data.Count.Should().Be(3, "Should be 3 items in the cache"); people = new[] - { - new Person("A", 3), - new Person("B", 4) - }; + { + new Person("A", 3), + new Person("B", 4) + }; subject.OnNext(people); @@ -68,44 +92,23 @@ public void OnNextProducesAnAddAndRemoveChangeForEnumerableSource() } [Fact] - public void LimitSizeTo() - { - var subject = new Subject>(); - var scheduler = new TestScheduler(); - var results = subject.ToObservableChangeSet(limitSizeTo: 100, scheduler: scheduler).AsAggregator(); - - var people = Enumerable.Range(1, 200).Select(i => new Person("p" + i.ToString("000"), i)).ToArray(); - - subject.OnNext(people); - - scheduler.AdvanceBy(1); - - results.Messages.Sum(x => x.Adds).Should().Be(200, "Should be 200 adds"); - results.Messages.Sum(x => x.Removes).Should().Be(100, "Should be 100 removes"); - results.Data.Count.Should().Be(100, "Should be 1 item in the cache"); - - var expected = people.Skip(100).ToArray().OrderBy(p => p.Name).ToArray(); - var actual = results.Data.Items.OrderBy(p => p.Name).ToArray(); - actual.Should().BeEquivalentTo(expected, "Only second hundred should be in the cache"); - } - - [Fact] - public void ExpireAfterTime() + public void OnNextProducesAnAddChangeForEnumerableSource() { var subject = new Subject>(); - var scheduler = new TestScheduler(); - var results = subject.ToObservableChangeSet(t => TimeSpan.FromMinutes(1), scheduler: scheduler).AsAggregator(); + var results = subject.ToObservableChangeSet().AsAggregator(); - var people = Enumerable.Range(1, 200).Select(i => new Person("p" + i.ToString("000"), i)).ToArray(); + var people = new[] + { + new Person("A", 1), + new Person("B", 2), + new Person("C", 3) + }; subject.OnNext(people); - scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); - //scheduler.Start(); - results.Messages.Count.Should().Be(2, "Should be 300 messages"); - results.Messages.Sum(x => x.Adds).Should().Be(200, "Should be 200 adds"); - results.Messages.Sum(x => x.Removes).Should().Be(200, "Should be 100 removes"); - results.Data.Count.Should().Be(0, "Should be no data in the cache"); + results.Messages.Count.Should().Be(1, "Should be 1 updates"); + results.Data.Count.Should().Be(3, "Should be 1 item in the cache"); + results.Data.Items.Should().BeEquivalentTo(results.Data.Items, "Lists should be equivalent"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ExceptFixture.cs b/src/DynamicData.Tests/Cache/ExceptFixture.cs index 2e023d42f..fc05d7afc 100644 --- a/src/DynamicData.Tests/Cache/ExceptFixture.cs +++ b/src/DynamicData.Tests/Cache/ExceptFixture.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class ExceptFixture : ExceptFixtureBase { protected override IObservable> CreateObservable() @@ -24,11 +26,13 @@ protected override IObservable> CreateObservable() } } - public abstract class ExceptFixtureBase: IDisposable + public abstract class ExceptFixtureBase : IDisposable { - protected ISourceCache _targetSource; protected ISourceCache _exceptSource; - private ChangeSetAggregator _results; + + protected ISourceCache _targetSource; + + private readonly ChangeSetAggregator _results; protected ExceptFixtureBase() { @@ -37,8 +41,6 @@ protected ExceptFixtureBase() _results = CreateObservable().AsAggregator(); } - protected abstract IObservable> CreateObservable(); - public void Dispose() { _targetSource.Dispose(); @@ -46,16 +48,6 @@ public void Dispose() _results.Dispose(); } - [Fact] - public void UpdatingOneSourceOnlyProducesResult() - { - var person = new Person("Adult1", 50); - _targetSource.AddOrUpdate(person); - - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - } - [Fact] public void DoNotIncludeExceptListItems() { @@ -78,5 +70,17 @@ public void RemovedAnItemFromExceptThenIncludesTheItem() _results.Messages.Count.Should().Be(1, "Should be 2 updates"); _results.Data.Count.Should().Be(1, "Cache should have no items"); } + + [Fact] + public void UpdatingOneSourceOnlyProducesResult() + { + var person = new Person("Adult1", 50); + _targetSource.AddOrUpdate(person); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + } + + protected abstract IObservable> CreateObservable(); } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ExpireAfterFixture.cs b/src/DynamicData.Tests/Cache/ExpireAfterFixture.cs index 0613667cd..f33d30e6a 100644 --- a/src/DynamicData.Tests/Cache/ExpireAfterFixture.cs +++ b/src/DynamicData.Tests/Cache/ExpireAfterFixture.cs @@ -1,19 +1,24 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { - - public class ExpireAfterFixture: IDisposable + public class ExpireAfterFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; + private readonly ISourceCache _source; + public ExpireAfterFixture() { _scheduler = new TestScheduler(); @@ -21,10 +26,21 @@ public ExpireAfterFixture() _results = _source.Connect().AsAggregator(); } - public void Dispose() + [Fact] + public void CanHandleABatchOfUpdates() { - _results.Dispose(); - _source.Dispose(); + var remover = _source.ExpireAfter(p => TimeSpan.FromMilliseconds(100), _scheduler).Subscribe(); + const int size = 100; + Person[] items = Enumerable.Range(1, size).Select(i => new Person($"Name.{i}", i)).ToArray(); + + _source.AddOrUpdate(items); + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(200).Ticks); + remover.Dispose(); + + _results.Data.Count.Should().Be(0, "Should be no data in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(100, "Should be 100 adds in the first message"); + _results.Messages[1].Removes.Should().Be(100, "Should be 100 removes in the second message"); } [Fact] @@ -60,19 +76,10 @@ public void ComplexRemove() remover.Dispose(); } - [Fact] - public void ItemAddedIsExpired() + public void Dispose() { - var remover = _source.ExpireAfter(p => TimeSpan.FromMilliseconds(100), _scheduler).Subscribe(); - - _source.AddOrUpdate(new Person("Name1", 10)); - - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(200).Ticks); - remover.Dispose(); - - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds in the first update"); - _results.Messages[1].Removes.Should().Be(1, "Should be 1 removes in the second update"); + _results.Dispose(); + _source.Dispose(); } [Fact] @@ -80,11 +87,12 @@ public void ExpireIsCancelledWhenUpdated() { var remover = _source.ExpireAfter(p => TimeSpan.FromMilliseconds(100), _scheduler).Subscribe(); - _source.Edit(updater => - { - updater.AddOrUpdate(new Person("Name1", 20)); - updater.AddOrUpdate(new Person("Name1", 21)); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person("Name1", 20)); + updater.AddOrUpdate(new Person("Name1", 21)); + }); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(200).Ticks); remover.Dispose(); @@ -96,20 +104,18 @@ public void ExpireIsCancelledWhenUpdated() } [Fact] - public void CanHandleABatchOfUpdates() + public void ItemAddedIsExpired() { var remover = _source.ExpireAfter(p => TimeSpan.FromMilliseconds(100), _scheduler).Subscribe(); - const int size = 100; - Person[] items = Enumerable.Range(1, size).Select(i => new Person($"Name.{i}", i)).ToArray(); - _source.AddOrUpdate(items); + _source.AddOrUpdate(new Person("Name1", 10)); + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(200).Ticks); remover.Dispose(); - _results.Data.Count.Should().Be(0, "Should be no data in the cache"); _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should be 100 adds in the first message"); - _results.Messages[1].Removes.Should().Be(100, "Should be 100 removes in the second message"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds in the first update"); + _results.Messages[1].Removes.Should().Be(1, "Should be 1 removes in the second update"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/FilterControllerFixture.cs b/src/DynamicData.Tests/Cache/FilterControllerFixture.cs index f65a8e0cb..3f82e367b 100644 --- a/src/DynamicData.Tests/Cache/FilterControllerFixture.cs +++ b/src/DynamicData.Tests/Cache/FilterControllerFixture.cs @@ -1,134 +1,32 @@ using System; using System.Linq; +using System.Reactive; +using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; -using System.Reactive; -using System.Reactive.Linq; namespace DynamicData.Tests.Cache { - - public class FilterControllerFixture: IDisposable + public class FilterControllerFixture : IDisposable { - private readonly ISourceCache _source; - private readonly ChangeSetAggregator _results; private readonly ISubject> _filter; - public FilterControllerFixture() + private readonly ChangeSetAggregator _results; + + private readonly ISourceCache _source; + + public FilterControllerFixture() { _source = new SourceCache(p => p.Key); _filter = new BehaviorSubject>(p => p.Age > 20); _results = new ChangeSetAggregator(_source.Connect().Filter(_filter)); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - - [Fact] - public void ChangeFilter() - { - var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); - - _source.AddOrUpdate(people); - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - - _filter.OnNext(p => p.Age <= 50); - _results.Data.Count.Should().Be(50, "Should be 50 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - _results.Messages[1].Removes.Should().Be(50, "Should be 50 removes in the second message"); - _results.Messages[1].Adds.Should().Be(20, "Should be 20 adds in the second message"); - - _results.Data.Items.All(p => p.Age <= 50).Should().BeTrue(); - } - - [Fact] - public void ReapplyFilterDoesntThrow() - { - using (var source = new SourceCache(p => p.Key)) - { - source.AddOrUpdate(Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray()); - - var ex = Record.Exception(() => source.Connect() - .Filter(Observable.Return(Unit.Default)) - .AsObservableCache()); - Assert.Null(ex); - } - } - - [Fact] - public void RepeatedApply() - { - using (var source = new SourceCache(p => p.Key)) - { - source.AddOrUpdate(Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray()); - _filter.OnNext(p => true); - - IChangeSet latestChanges = null; - using (source.Connect().Filter(_filter) - .Do(changes => latestChanges = changes).AsObservableCache()) - { - _filter.OnNext(p => false); - latestChanges.Removes.Should().Be(100); - latestChanges.Adds.Should().Be(0); - - _filter.OnNext(p => true); - latestChanges.Adds.Should().Be(100); - latestChanges.Removes.Should().Be(0); - - _filter.OnNext(p => false); - latestChanges.Removes.Should().Be(100); - latestChanges.Adds.Should().Be(0); - - _filter.OnNext(p => true); - latestChanges.Adds.Should().Be(100); - latestChanges.Removes.Should().Be(0); - - _filter.OnNext(p => false); - latestChanges.Removes.Should().Be(100); - latestChanges.Adds.Should().Be(0); - } - } - } - - [Fact] - public void ReevaluateFilter() - { - //re-evaluate for inline changes - var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); - - _source.AddOrUpdate(people); - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - - foreach (var person in people) - { - person.Age = person.Age + 10; - } - - _filter.OnNext(p => p.Age > 20); - - _results.Data.Count.Should().Be(90, "Should be 90 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - _results.Messages[1].Adds.Should().Be(10, "Should be 10 adds in the second message"); - - foreach (var person in people) - { - person.Age = person.Age - 10; - } - - _filter.OnNext(p => p.Age > 20); - - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - _results.Messages.Count.Should().Be(3, "Should be 3 update messages"); - _results.Messages[2].Removes.Should().Be(10, "Should be 10 removes in the third message"); - } - - #region Static filter tests - /* Should be the same as standard lambda filter */ [Fact] @@ -159,11 +57,12 @@ public void AddNotMatchedAndUpdateMatched() var notmatched = new Person(key, 19); var matched = new Person(key, 21); - _source.Edit(innerCache => - { - innerCache.AddOrUpdate(notmatched); - innerCache.AddOrUpdate(matched); - }); + _source.Edit( + innerCache => + { + innerCache.AddOrUpdate(notmatched); + innerCache.AddOrUpdate(matched); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].First().Current.Should().Be(matched, "Should be same person"); @@ -221,6 +120,23 @@ public void BatchSuccessiveUpdates() _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(filtered, "Incorrect Filter result"); } + [Fact] + public void ChangeFilter() + { + var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); + + _source.AddOrUpdate(people); + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + + _filter.OnNext(p => p.Age <= 50); + _results.Data.Count.Should().Be(50, "Should be 50 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + _results.Messages[1].Removes.Should().Be(50, "Should be 50 removes in the second message"); + _results.Messages[1].Adds.Should().Be(20, "Should be 20 adds in the second message"); + + _results.Data.Items.All(p => p.Age <= 50).Should().BeTrue(); + } + [Fact] public void Clear() { @@ -234,6 +150,54 @@ public void Clear() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void ReapplyFilterDoesntThrow() + { + using var source = new SourceCache(p => p.Key); + source.AddOrUpdate(Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray()); + + var ex = Record.Exception(() => source.Connect().Filter(Observable.Return(Unit.Default)).AsObservableCache()); + Assert.Null(ex); + } + + [Fact] + public void ReevaluateFilter() + { + //re-evaluate for inline changes + var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); + + _source.AddOrUpdate(people); + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + + foreach (var person in people) + { + person.Age += 10; + } + + _filter.OnNext(p => p.Age > 20); + + _results.Data.Count.Should().Be(90, "Should be 90 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + _results.Messages[1].Adds.Should().Be(10, "Should be 10 adds in the second message"); + + foreach (var person in people) + { + person.Age -= 10; + } + + _filter.OnNext(p => p.Age > 20); + + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + _results.Messages.Count.Should().Be(3, "Should be 3 update messages"); + _results.Messages[2].Removes.Should().Be(10, "Should be 10 removes in the third message"); + } + [Fact] public void Remove() { @@ -251,18 +215,40 @@ public void Remove() } [Fact] - public void UpdateMatched() + public void RepeatedApply() { - const string key = "Adult1"; - var newperson = new Person(key, 50); - var updated = new Person(key, 51); + using var source = new SourceCache(p => p.Key); + source.AddOrUpdate(Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray()); + _filter.OnNext(p => true); - _source.AddOrUpdate(newperson); - _source.AddOrUpdate(updated); + IChangeSet? latestChanges = null; + using (source.Connect().Filter(_filter).Do(changes => latestChanges = changes).AsObservableCache()) + { + if (latestChanges is null) + { + throw new InvalidOperationException(nameof(latestChanges)); + } - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - _results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); + _filter.OnNext(p => false); + latestChanges.Removes.Should().Be(100); + latestChanges.Adds.Should().Be(0); + + _filter.OnNext(p => true); + latestChanges.Adds.Should().Be(100); + latestChanges.Removes.Should().Be(0); + + _filter.OnNext(p => false); + latestChanges.Removes.Should().Be(100); + latestChanges.Adds.Should().Be(0); + + _filter.OnNext(p => true); + latestChanges.Adds.Should().Be(100); + latestChanges.Removes.Should().Be(0); + + _filter.OnNext(p => false); + latestChanges.Removes.Should().Be(100); + latestChanges.Adds.Should().Be(0); + } } [Fact] @@ -270,13 +256,14 @@ public void SameKeyChanges() { const string key = "Adult1"; - _source.Edit(updater => - { - updater.AddOrUpdate(new Person(key, 50)); - updater.AddOrUpdate(new Person(key, 52)); - updater.AddOrUpdate(new Person(key, 53)); - updater.Remove(key); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(key, 50)); + updater.AddOrUpdate(new Person(key, 52)); + updater.AddOrUpdate(new Person(key, 53)); + updater.Remove(key); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); @@ -284,6 +271,21 @@ public void SameKeyChanges() _results.Messages[0].Removes.Should().Be(1, "Should be 1 remove"); } + [Fact] + public void UpdateMatched() + { + const string key = "Adult1"; + var newperson = new Person(key, 50); + var updated = new Person(key, 51); + + _source.AddOrUpdate(newperson); + _source.AddOrUpdate(updated); + + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); + _results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); + } + [Fact] public void UpdateNotMatched() { @@ -297,7 +299,5 @@ public void UpdateNotMatched() _results.Messages.Count.Should().Be(0, "Should be no updates"); _results.Data.Count.Should().Be(0, "Should nothing cached"); } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/FilterFixture.cs b/src/DynamicData.Tests/Cache/FilterFixture.cs index 3f0397b2c..f3670ba0e 100644 --- a/src/DynamicData.Tests/Cache/FilterFixture.cs +++ b/src/DynamicData.Tests/Cache/FilterFixture.cs @@ -1,30 +1,27 @@ using System; using System.Linq; using System.Reactive.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class FilterFixture: IDisposable + public class FilterFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; + private readonly ISourceCache _source; + public FilterFixture() { _source = new SourceCache(p => p.Name); _results = _source.Connect(p => p.Age > 20).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void AddMatched() { @@ -53,11 +50,12 @@ public void AddNotMatchedAndUpdateMatched() var notmatched = new Person(key, 19); var matched = new Person(key, 21); - _source.Edit(updater => - { - updater.AddOrUpdate(notmatched); - updater.AddOrUpdate(matched); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(notmatched); + updater.AddOrUpdate(matched); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].First().Current.Should().Be(matched, "Should be same person"); @@ -129,6 +127,27 @@ public void Clear() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void DuplicateKeyWithMerge() + { + const string key = "Adult1"; + var newperson = new Person(key, 30); + + using var results = _source.Connect().Merge(_source.Connect()).Filter(p => p.Age > 20).AsAggregator(); + _source.AddOrUpdate(newperson); // previously this would throw an exception + + results.Messages.Count.Should().Be(2, "Should be 2 messages"); + results.Messages[0].Adds.Should().Be(1, "Should be 1 add"); + results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); + results.Data.Count.Should().Be(1, "Should be cached"); + } + [Fact] public void Remove() { @@ -145,6 +164,26 @@ public void Remove() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + [Fact] + public void SameKeyChanges() + { + const string key = "Adult1"; + + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(key, 50)); + updater.AddOrUpdate(new Person(key, 52)); + updater.AddOrUpdate(new Person(key, 53)); + updater.Remove(key); + }); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); + _results.Messages[0].Updates.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Removes.Should().Be(1, "Should be 1 remove"); + } + [Fact] public void UpdateMatched() @@ -160,25 +199,6 @@ public void UpdateMatched() _results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); } - [Fact] - public void SameKeyChanges() - { - const string key = "Adult1"; - - _source.Edit(updater => - { - updater.AddOrUpdate(new Person(key, 50)); - updater.AddOrUpdate(new Person(key, 52)); - updater.AddOrUpdate(new Person(key, 53)); - updater.Remove(key); - }); - - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - _results.Messages[0].Updates.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Removes.Should().Be(1, "Should be 1 remove"); - } - [Fact] public void UpdateNotMatched() { @@ -192,24 +212,5 @@ public void UpdateNotMatched() _results.Messages.Count.Should().Be(0, "Should be no updates"); _results.Data.Count.Should().Be(0, "Should nothing cached"); } - - [Fact] - public void DuplicateKeyWithMerge() - { - const string key = "Adult1"; - var newperson = new Person(key, 30); - - using (var results = _source.Connect() - .Merge(_source.Connect()) - .Filter(p => p.Age > 20).AsAggregator()) - { - _source.AddOrUpdate(newperson); // previously this would throw an exception - - results.Messages.Count.Should().Be(2, "Should be 2 messages"); - results.Messages[0].Adds.Should().Be(1, "Should be 1 add"); - results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); - results.Data.Count.Should().Be(1, "Should be cached"); - } - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/FilterOnPropertyFixture.cs b/src/DynamicData.Tests/Cache/FilterOnPropertyFixture.cs index 3e96b401a..7d9b3ba41 100644 --- a/src/DynamicData.Tests/Cache/FilterOnPropertyFixture.cs +++ b/src/DynamicData.Tests/Cache/FilterOnPropertyFixture.cs @@ -1,87 +1,79 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class FilterOnPropertyFixture { [Fact] - public void InitialValues() + public void ChangeAValueSoItIsStillInTheFilter() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddOrUpdate(people); + using var stub = new FilterPropertyStub(); + stub.Source.AddOrUpdate(people); - 1.Should().Be(stub.Results.Messages.Count); - 82.Should().Be(stub.Results.Data.Count); + people[50].Age = 100; - stub.Results.Data.Items.Should().BeEquivalentTo(people.Skip(18)); - } + stub.Results.Messages.Count.Should().Be(2); + stub.Results.Data.Count.Should().Be(82); } [Fact] public void ChangeAValueToMatchFilter() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddOrUpdate(people); + using var stub = new FilterPropertyStub(); + stub.Source.AddOrUpdate(people); - people[20].Age = 10; + people[20].Age = 10; - 2.Should().Be(stub.Results.Messages.Count); - 81.Should().Be(stub.Results.Data.Count); - } + 2.Should().Be(stub.Results.Messages.Count); + 81.Should().Be(stub.Results.Data.Count); } [Fact] public void ChangeAValueToNoLongerMatchFilter() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddOrUpdate(people); + using var stub = new FilterPropertyStub(); + stub.Source.AddOrUpdate(people); - people[10].Age = 20; + people[10].Age = 20; - 2.Should().Be(stub.Results.Messages.Count); - 83.Should().Be(stub.Results.Data.Count); - } + 2.Should().Be(stub.Results.Messages.Count); + 83.Should().Be(stub.Results.Data.Count); } [Fact] - public void ChangeAValueSoItIsStillInTheFilter() + public void InitialValues() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddOrUpdate(people); + using var stub = new FilterPropertyStub(); + stub.Source.AddOrUpdate(people); - people[50].Age = 100; + 1.Should().Be(stub.Results.Messages.Count); + 82.Should().Be(stub.Results.Data.Count); - stub.Results.Messages.Count.Should().Be(2); - stub.Results.Data.Count.Should().Be(82); - } + stub.Results.Data.Items.Should().BeEquivalentTo(people.Skip(18)); } private class FilterPropertyStub : IDisposable { - public ISourceCache Source { get; } = new SourceCache(p => p.Name); - public ChangeSetAggregator Results { get; } - public FilterPropertyStub() { - Results = new ChangeSetAggregator - ( - Source.Connect().FilterOnProperty(p => p.Age, p => p.Age > 18) - ); + Results = new ChangeSetAggregator(Source.Connect().FilterOnProperty(p => p.Age, p => p.Age > 18)); } + public ChangeSetAggregator Results { get; } + + public ISourceCache Source { get; } = new SourceCache(p => p.Name); + public void Dispose() { Source.Dispose(); diff --git a/src/DynamicData.Tests/Cache/FilterParallelFixture.cs b/src/DynamicData.Tests/Cache/FilterParallelFixture.cs index 53da82ff0..79e194f92 100644 --- a/src/DynamicData.Tests/Cache/FilterParallelFixture.cs +++ b/src/DynamicData.Tests/Cache/FilterParallelFixture.cs @@ -1,30 +1,27 @@ using System; using System.Linq; + using DynamicData.PLinq; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class FilterParallelFixture: IDisposable + public class FilterParallelFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; + private readonly ISourceCache _source; + public FilterParallelFixture() { _source = new SourceCache(p => p.Key); _results = new ChangeSetAggregator(_source.Connect().Filter(p => p.Age > 20, new ParallelisationOptions(ParallelType.Ordered))); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void AddMatched() { @@ -53,11 +50,12 @@ public void AddNotMatchedAndUpdateMatched() var notmatched = new Person(key, 19); var matched = new Person(key, 21); - _source.Edit(updater => - { - updater.AddOrUpdate(notmatched); - updater.AddOrUpdate(matched); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(notmatched); + updater.AddOrUpdate(matched); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].First().Current.Should().Be(matched, "Should be same person"); @@ -128,6 +126,12 @@ public void Clear() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + [Fact] public void Remove() { @@ -145,37 +149,38 @@ public void Remove() } [Fact] - public void UpdateMatched() + public void SameKeyChanges() { const string key = "Adult1"; - var newperson = new Person(key, 50); - var updated = new Person(key, 51); - _source.AddOrUpdate(newperson); - _source.AddOrUpdate(updated); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(key, 50)); + updater.AddOrUpdate(new Person(key, 52)); + updater.AddOrUpdate(new Person(key, 53)); + updater.Remove(key); + }); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - _results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); + _results.Messages[0].Updates.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Removes.Should().Be(1, "Should be 1 remove"); } [Fact] - public void SameKeyChanges() + public void UpdateMatched() { const string key = "Adult1"; + var newperson = new Person(key, 50); + var updated = new Person(key, 51); - _source.Edit(updater => - { - updater.AddOrUpdate(new Person(key, 50)); - updater.AddOrUpdate(new Person(key, 52)); - updater.AddOrUpdate(new Person(key, 53)); - updater.Remove(key); - }); + _source.AddOrUpdate(newperson); + _source.AddOrUpdate(updated); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - _results.Messages[0].Updates.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Removes.Should().Be(1, "Should be 1 remove"); + _results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); } [Fact] @@ -192,5 +197,4 @@ public void UpdateNotMatched() _results.Data.Count.Should().Be(0, "Should nothing cached"); } } -} - +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ForEachChangeFixture.cs b/src/DynamicData.Tests/Cache/ForEachChangeFixture.cs index 7b120c066..354f96a3f 100644 --- a/src/DynamicData.Tests/Cache/ForEachChangeFixture.cs +++ b/src/DynamicData.Tests/Cache/ForEachChangeFixture.cs @@ -1,17 +1,19 @@ +using System; using System.Collections.Generic; + using DynamicData.Tests.Domain; -using Xunit; -using System; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - - public class ForEachChangeFixture: IDisposable + public class ForEachChangeFixture : IDisposable { private readonly ISourceCache _source; - public ForEachChangeFixture() + public ForEachChangeFixture() { _source = new SourceCache(p => p.Name); } @@ -25,10 +27,7 @@ public void Dispose() public void Test() { var messages = new List>(); - var messageWriter = _source - .Connect() - .ForEachChange(messages.Add) - .Subscribe(); + var messageWriter = _source.Connect().ForEachChange(messages.Add).Subscribe(); _source.AddOrUpdate(new RandomPersonGenerator().Take(100)); messageWriter.Dispose(); @@ -36,4 +35,4 @@ public void Test() messages.Count.Should().Be(100); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/FromAsyncFixture.cs b/src/DynamicData.Tests/Cache/FromAsyncFixture.cs index 773443ef3..50c27b211 100644 --- a/src/DynamicData.Tests/Cache/FromAsyncFixture.cs +++ b/src/DynamicData.Tests/Cache/FromAsyncFixture.cs @@ -3,39 +3,37 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { - public class FromAsyncFixture { - public TestScheduler Scheduler { get; } - - public FromAsyncFixture() + public FromAsyncFixture() { Scheduler = new TestScheduler(); } + public TestScheduler Scheduler { get; } + [Fact] public void CanLoadFromTask() { Task> Loader() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, 1)) - .ToArray() - .AsEnumerable(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, 1)).ToArray().AsEnumerable(); return Task.FromResult(items); } - var data = Observable.FromAsync((Func>>) Loader) - .ToObservableChangeSet(p=>p.Key) - .AsObservableCache(); + var data = Observable.FromAsync((Func>>)Loader).ToObservableChangeSet(p => p.Key).AsObservableCache(); data.Count.Should().Be(100); } @@ -49,11 +47,9 @@ Task> Loader() throw new Exception("Broken"); } - Exception error = null; + Exception? error = null; - var data = Observable.FromAsync((Func>>) Loader) - .ToObservableChangeSet(p => p.Key) - .Subscribe((changes) => { }, ex => error = ex); + var data = Observable.FromAsync((Func>>)Loader).ToObservableChangeSet(p => p.Key).Subscribe((changes) => { }, ex => error = ex); error.Should().NotBeNull(); } @@ -66,23 +62,16 @@ Task> Loader() throw new Exception("Broken"); } - Exception error = null; + Exception? error = null; - var data = Observable.FromAsync(Loader) - .ToObservableChangeSet(p => p.Key) - .Subscribe(changes => { }, ex => error = ex); + var data = Observable.FromAsync(Loader).ToObservableChangeSet(p => p.Key).Subscribe(changes => { }, ex => error = ex); - var data2 = Observable.FromAsync(Loader) - .ToObservableChangeSet(p => p.Key) - .AsObservableCache() - .Connect() - .Subscribe(changes => { }, ex => error = ex); + var data2 = Observable.FromAsync(Loader).ToObservableChangeSet(p => p.Key).AsObservableCache().Connect().Subscribe(changes => { }, ex => error = ex); //var subscribed = data.Connect() // error.Should().NotBeNull(); } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/FullJoinFixture.cs b/src/DynamicData.Tests/Cache/FullJoinFixture.cs index 7004353a2..dde7e277d 100644 --- a/src/DynamicData.Tests/Cache/FullJoinFixture.cs +++ b/src/DynamicData.Tests/Cache/FullJoinFixture.cs @@ -1,47 +1,40 @@ using System; using System.Linq; + using DynamicData.Kernel; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - public class FullJoinFixture: IDisposable + public class FullJoinFixture : IDisposable { - private class Person - { - public int Id { get; } - } + private readonly SourceCache _left; - private class Address - { - public int Id { get; } - public int PersonId { get; } - } + private readonly ChangeSetAggregator _result; - private readonly SourceCache _left; private readonly SourceCache _right; - private readonly ChangeSetAggregator _result; public FullJoinFixture() { _left = new SourceCache(device => device.Name); _right = new SourceCache(device => device.Name); - _result = _left.Connect() - .FullJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(key, device, meta)) - .AsAggregator(); + _result = _left.Connect().FullJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(key, device, meta)).AsAggregator(); } [Fact] public void AddLeftOnly() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Lookup("Device1").HasValue.Should().BeTrue(); @@ -52,15 +45,40 @@ public void AddLeftOnly() _result.Data.Items.All(dwm => dwm.Device != Optional.None).Should().BeTrue(); } + [Fact] + public void AddLetThenRight() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + 3.Should().Be(_result.Data.Count); + + _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); + } + [Fact] public void AddRightOnly() { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Lookup("Device1").HasValue.Should().BeTrue(); @@ -71,43 +89,54 @@ public void AddRightOnly() } [Fact] - public void AddLetThenRight() + public void AddRightThenLeft() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); } + public void Dispose() + { + _left.Dispose(); + _right.Dispose(); + _result.Dispose(); + } + [Fact] public void RemoveVarious() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); _result.Data.Lookup("Device1").HasValue.Should().BeTrue(); _result.Data.Lookup("Device2").HasValue.Should().BeTrue(); _result.Data.Lookup("Device3").HasValue.Should().BeTrue(); @@ -123,69 +152,50 @@ public void RemoveVarious() _result.Data.Lookup("Device3").HasValue.Should().BeTrue(); } - [Fact] - public void AddRightThenLeft() - { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); - - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - 3.Should().Be(_result.Data.Count); - - _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); - } - [Fact] public void UpdateRight() { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); - - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); } - public void Dispose() - { - _left.Dispose(); - _right.Dispose(); - _result.Dispose(); - } - public class Device : IEquatable { - public string Name { get; } - public Device(string name) { Name = name; } - #region Equality Members + public string Name { get; } + + public static bool operator ==(Device left, Device right) + { + return Equals(left, right); + } + + public static bool operator !=(Device left, Device right) + { + return !Equals(left, right); + } - public bool Equals(Device other) + public bool Equals(Device? other) { if (ReferenceEquals(null, other)) { @@ -200,7 +210,7 @@ public bool Equals(Device other) return string.Equals(Name, other.Name); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -222,21 +232,9 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Name != null ? Name.GetHashCode() : 0); + return (Name is not null ? Name.GetHashCode() : 0); } - public static bool operator ==(Device left, Device right) - { - return Equals(left, right); - } - - public static bool operator !=(Device left, Device right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{Name}"; @@ -245,19 +243,27 @@ public override string ToString() public class DeviceMetaData : IEquatable { - public string Name { get; } - - public bool IsAutoConnect { get; } - public DeviceMetaData(string name, bool isAutoConnect = false) { Name = name; IsAutoConnect = isAutoConnect; } - #region Equality members + public bool IsAutoConnect { get; } + + public string Name { get; } + + public static bool operator ==(DeviceMetaData left, DeviceMetaData right) + { + return Equals(left, right); + } + + public static bool operator !=(DeviceMetaData left, DeviceMetaData right) + { + return !Equals(left, right); + } - public bool Equals(DeviceMetaData other) + public bool Equals(DeviceMetaData? other) { if (ReferenceEquals(null, other)) { @@ -272,7 +278,7 @@ public bool Equals(DeviceMetaData other) return string.Equals(Name, other.Name) && IsAutoConnect == other.IsAutoConnect; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -296,22 +302,10 @@ public override int GetHashCode() { unchecked { - return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ IsAutoConnect.GetHashCode(); + return ((Name is not null ? Name.GetHashCode() : 0) * 397) ^ IsAutoConnect.GetHashCode(); } } - public static bool operator ==(DeviceMetaData left, DeviceMetaData right) - { - return Equals(left, right); - } - - public static bool operator !=(DeviceMetaData left, DeviceMetaData right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"Metadata: {Name}. IsAutoConnect = {IsAutoConnect}"; @@ -320,10 +314,6 @@ public override string ToString() public class DeviceWithMetadata : IEquatable { - public string Key { get; } - public Optional Device { get; set; } - public Optional MetaData { get; } - public DeviceWithMetadata(string key, Optional device, Optional metaData) { Key = key; @@ -331,9 +321,23 @@ public DeviceWithMetadata(string key, Optional device, Optional Device { get; set; } + + public string Key { get; } + + public Optional MetaData { get; } + + public static bool operator ==(DeviceWithMetadata left, DeviceWithMetadata right) + { + return Equals(left, right); + } + + public static bool operator !=(DeviceWithMetadata left, DeviceWithMetadata right) + { + return !Equals(left, right); + } - public bool Equals(DeviceWithMetadata other) + public bool Equals(DeviceWithMetadata? other) { if (ReferenceEquals(null, other)) { @@ -348,7 +352,7 @@ public bool Equals(DeviceWithMetadata other) return string.Equals(Key, other.Key); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -370,25 +374,25 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Key != null ? Key.GetHashCode() : 0); + return (Key is not null ? Key.GetHashCode() : 0); } - public static bool operator ==(DeviceWithMetadata left, DeviceWithMetadata right) + public override string ToString() { - return Equals(left, right); + return $"{Key}: {Device} ({MetaData})"; } + } - public static bool operator !=(DeviceWithMetadata left, DeviceWithMetadata right) - { - return !Equals(left, right); - } + private class Address + { + public int Id { get; } - #endregion + public int PersonId { get; } + } - public override string ToString() - { - return $"{Key}: {Device} ({MetaData})"; - } + private class Person + { + public int Id { get; } } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/FullJoinManyFixture.cs b/src/DynamicData.Tests/Cache/FullJoinManyFixture.cs index b3f728dd5..f059fa91d 100644 --- a/src/DynamicData.Tests/Cache/FullJoinManyFixture.cs +++ b/src/DynamicData.Tests/Cache/FullJoinManyFixture.cs @@ -1,37 +1,51 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class FullJoinManyFixture: IDisposable + public class FullJoinManyFixture : IDisposable { private readonly SourceCache _people; + private readonly ChangeSetAggregator _result; public FullJoinManyFixture() { _people = new SourceCache(p => p.Name); - _result = _people.Connect() - .FullJoinMany(_people.Connect(), pac => pac.ParentName, (personid, person, grouping) => new ParentAndChildren(personid, person, grouping.Items.Select(p => p).ToArray())) - .AsAggregator(); + _result = _people.Connect().FullJoinMany(_people.Connect(), pac => pac.ParentName, (personid, person, grouping) => new ParentAndChildren(personid, person, grouping.Items.Select(p => p).ToArray())).AsAggregator(); } - public void Dispose() + [Fact] + public void AddChild() { - _people.Dispose(); - _result.Dispose(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); + + _people.AddOrUpdate(people); + + var person11 = new Person("Person11", 100, parentName: "Person3"); + _people.AddOrUpdate(person11); + + var updatedPeople = people.Union(new[] { person11 }).ToArray(); + + AssertDataIsCorrectlyFormed(updatedPeople); } [Fact] public void AddLeftOnly() { - var people = Enumerable.Range(1, 1000) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + var people = Enumerable.Range(1, 1000).Select(i => new Person("Person" + i, i)).ToArray(); _people.AddOrUpdate(people); AssertDataIsCorrectlyFormed(people); @@ -40,36 +54,39 @@ public void AddLeftOnly() [Fact] public void AddPeopleWithParents() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); AssertDataIsCorrectlyFormed(people); } + public void Dispose() + { + _people.Dispose(); + _result.Dispose(); + } + [Fact] - public void UpdateParent() + public void RemoveChild() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); - var current10 = people.Last(); - var person10 = new Person("Person10", 100, parentName: current10.ParentName); - _people.AddOrUpdate(person10); + var last = people.Last(); + _people.Remove(last); - var updatedPeople = people.Take(9).Union(new[] { person10 }).ToArray(); + var updatedPeople = people.Where(p => p.Name != last.Name).ToArray(); AssertDataIsCorrectlyFormed(updatedPeople); } @@ -77,13 +94,12 @@ public void UpdateParent() [Fact] public void UpdateChild() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); @@ -97,43 +113,22 @@ public void UpdateChild() } [Fact] - public void AddChild() - { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); - - _people.AddOrUpdate(people); - - var person11 = new Person("Person11", 100, parentName: "Person3"); - _people.AddOrUpdate(person11); - - var updatedPeople = people.Union(new[] { person11 }).ToArray(); - - AssertDataIsCorrectlyFormed(updatedPeople); - } - - [Fact] - public void RemoveChild() + public void UpdateParent() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); - var last = people.Last(); - _people.Remove(last); + var current10 = people.Last(); + var person10 = new Person("Person10", 100, parentName: current10.ParentName); + _people.AddOrUpdate(person10); - var updatedPeople = people.Where(p => p.Name != last.Name).ToArray(); + var updatedPeople = people.Take(9).Union(new[] { person10 }).ToArray(); AssertDataIsCorrectlyFormed(updatedPeople); } @@ -144,23 +139,29 @@ private void AssertDataIsCorrectlyFormed(Person[] allPeople) var parentNames = allPeople.Select(p => p.ParentName).Distinct(); var childrenNames = allPeople.Select(p => p.Name).Distinct(); - var all = parentNames.Union(childrenNames).Distinct() - .Select(key => - { - var parent = people.Lookup(key); - var children = people.Values.Where(p => p.ParentName == key).ToArray(); - return new ParentAndChildren(key, parent, children); - - }).ToArray(); + var all = parentNames.Union(childrenNames).Distinct().Select( + key => + { + var parent = people.Lookup(key); + var children = people.Values.Where(p => p.ParentName == key).ToArray(); + return new ParentAndChildren(key, parent, children); + }).ToArray(); _result.Data.Count.Should().Be(all.Length); - all.ForEach(parentAndChild => - { - var result = _result.Data.Lookup(parentAndChild.ParentId).ValueOr(() => null); - var children = result.Children; - children.Should().BeEquivalentTo(parentAndChild.Children); - }); + all.ForEach( + parentAndChild => + { + var result = parentAndChild.ParentId is null ? null : _result.Data.Lookup(parentAndChild.ParentId).ValueOrDefault(); + + if (result is null) + { + throw new InvalidOperationException(nameof(result)); + } + + var children = result.Children; + children.Should().BeEquivalentTo(parentAndChild.Children); + }); } private int CalculateParent(int index, int totalPeople) @@ -182,6 +183,5 @@ private int CalculateParent(int index, int totalPeople) return index + 1; } - } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/GroupControllerFixture.cs b/src/DynamicData.Tests/Cache/GroupControllerFixture.cs index a3d0a475d..74c8e738f 100644 --- a/src/DynamicData.Tests/Cache/GroupControllerFixture.cs +++ b/src/DynamicData.Tests/Cache/GroupControllerFixture.cs @@ -2,43 +2,49 @@ using System.Linq; using System.Reactive; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - - public class GroupControllerFixture: IDisposable + public class GroupControllerFixture : IDisposable { - private enum AgeBracket - { - Under20, - Adult, - Pensioner - } + private readonly IObservableCache, AgeBracket> _grouped; private readonly Func _grouper = p => - { - if (p.Age <= 19) { - return AgeBracket.Under20; - } + if (p.Age <= 19) + { + return AgeBracket.Under20; + } - return p.Age <= 60 ? AgeBracket.Adult : AgeBracket.Pensioner; - }; + return p.Age <= 60 ? AgeBracket.Adult : AgeBracket.Pensioner; + }; - private readonly ISourceCache _source; private readonly ISubject _refresher; - private readonly IObservableCache, AgeBracket> _grouped; + + private readonly ISourceCache _source; public GroupControllerFixture() { _source = new SourceCache(p => p.Name); - _refresher =new Subject(); + _refresher = new Subject(); _grouped = _source.Connect().Group(_grouper, _refresher).AsObservableCache(); } + private enum AgeBracket + { + Under20, + + Adult, + + Pensioner + } + public void Dispose() { _source?.Dispose(); @@ -86,7 +92,7 @@ public void RegroupRecaluatesGroupings2() var p2 = new Person("P2", 15); var p3 = new Person("P3", 30); var p4 = new Person("P4", 70); - var people = new[] {p1, p2, p3, p4}; + var people = new[] { p1, p2, p3, p4 }; _source.AddOrUpdate(people); @@ -102,7 +108,7 @@ public void RegroupRecaluatesGroupings2() // _refresher.RefreshGroup(); - _source.Refresh(new[] {p1, p2, p3, p4}); + _source.Refresh(new[] { p1, p2, p3, p4 }); IsContainedIn("P1", AgeBracket.Adult).Should().BeTrue(); IsContainedIn("P2", AgeBracket.Pensioner).Should().BeTrue(); @@ -132,6 +138,5 @@ private bool IsContainedOnlyInOneGroup(string name) return person.Count == 1; } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/GroupControllerForFilteredItemsFixture.cs b/src/DynamicData.Tests/Cache/GroupControllerForFilteredItemsFixture.cs index 56a5b3f27..871b58d9a 100644 --- a/src/DynamicData.Tests/Cache/GroupControllerForFilteredItemsFixture.cs +++ b/src/DynamicData.Tests/Cache/GroupControllerForFilteredItemsFixture.cs @@ -2,42 +2,47 @@ using System.Linq; using System.Reactive; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - - public class GroupControllerForFilteredItemsFixture: IDisposable + public class GroupControllerForFilteredItemsFixture : IDisposable { - private enum AgeBracket - { - Under20, - Adult, - Pensioner - } + private readonly IObservableCache, AgeBracket> _grouped; private readonly Func _grouper = p => - { - if (p.Age <= 19) { - return AgeBracket.Under20; - } + if (p.Age <= 19) + { + return AgeBracket.Under20; + } - return p.Age <= 60 ? AgeBracket.Adult : AgeBracket.Pensioner; - }; + return p.Age <= 60 ? AgeBracket.Adult : AgeBracket.Pensioner; + }; - private readonly ISourceCache _source; private readonly ISubject _refreshSubject = new Subject(); - private readonly IObservableCache, AgeBracket> _grouped; - public GroupControllerForFilteredItemsFixture() + private readonly ISourceCache _source; + + public GroupControllerForFilteredItemsFixture() { _source = new SourceCache(p => p.Name); - _grouped = _source.Connect(p => _grouper(p) != AgeBracket.Pensioner) - .Group(_grouper, _refreshSubject).AsObservableCache(); + _grouped = _source.Connect(p => _grouper(p) != AgeBracket.Pensioner).Group(_grouper, _refreshSubject).AsObservableCache(); + } + + private enum AgeBracket + { + Under20, + + Adult, + + Pensioner } public void Dispose() @@ -82,7 +87,7 @@ public void RegroupRecaluatesGroupings2() var p2 = new Person("P2", 15); var p3 = new Person("P3", 30); var p4 = new Person("P4", 70); - var people = new[] {p1, p2, p3, p4}; + var people = new[] { p1, p2, p3, p4 }; _source.AddOrUpdate(people); @@ -98,7 +103,7 @@ public void RegroupRecaluatesGroupings2() // _controller.RefreshGroup(); - _source.Refresh(new[] {p1, p2, p3, p4}); + _source.Refresh(new[] { p1, p2, p3, p4 }); IsContainedIn("P1", AgeBracket.Adult).Should().BeTrue(); IsContainedIn("P2", AgeBracket.Pensioner).Should().BeFalse(); @@ -135,6 +140,5 @@ private bool IsNotContainedAnyWhere(string name) return person.Count == 0; } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/GroupFixture.cs b/src/DynamicData.Tests/Cache/GroupFixture.cs index 664de4d02..6ee69d75d 100644 --- a/src/DynamicData.Tests/Cache/GroupFixture.cs +++ b/src/DynamicData.Tests/Cache/GroupFixture.cs @@ -2,39 +2,37 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class GroupFixture: IDisposable + public class GroupFixture : IDisposable { - public GroupFixture() - { - _source = new SourceCache(p => p.Name); - } + private readonly ISourceCache _source; - public void Dispose() + private ReadOnlyObservableCollection? _entries; + + public GroupFixture() { - _source.Dispose(); + _source = new SourceCache(p => p.Name); } - private readonly ISourceCache _source; - [Fact] public void Add() { bool called = false; - IDisposable subscriber = _source.Connect().Group(p => p.Age) - .Subscribe( - updates => - { - updates.Count.Should().Be(1, "Should be 1 add"); - updates.First().Reason.Should().Be(ChangeReason.Add); - called = true; - }); + IDisposable subscriber = _source.Connect().Group(p => p.Age).Subscribe( + updates => + { + updates.Count.Should().Be(1, "Should be 1 add"); + updates.First().Reason.Should().Be(ChangeReason.Add); + called = true; + }); _source.AddOrUpdate(new Person("Person1", 20)); subscriber.Dispose(); @@ -42,55 +40,37 @@ public void Add() } [Fact] - public void UpdateNotPossible() + public void AddItemAfterUpdateItemProcessAdd() { - bool called = false; - IDisposable subscriber = _source.Connect().Group(p => p.Age).Skip(1) - .Subscribe(updates => { called = true; }); - _source.AddOrUpdate(new Person("Person1", 20)); - _source.AddOrUpdate(new Person("Person1", 20)); - subscriber.Dispose(); - called.Should().BeFalse(); - } + var subscriber = _source.Connect().Group(x => x.Name[0].ToString()).Transform(x => new GroupViewModel(x)).Bind(out _entries).Subscribe(); + + _source.Edit(x => { x.AddOrUpdate(new Person("Adam", 1)); }); + + var firstGroup = _entries.First(); + firstGroup.Entries.Count.Should().Be(1); + + _source.Edit( + x => + { + x.AddOrUpdate(new Person("Adam", 3)); // update + x.AddOrUpdate(new Person("Alfred", 1)); // add + }); + + firstGroup.Entries.Count.Should().Be(2); - [Fact] - public void UpdateAnItemWillChangedThegroup() - { - bool called = false; - IDisposable subscriber = _source.Connect().Group(p => p.Age) - .Subscribe(updates => { called = true; }); - _source.AddOrUpdate(new Person("Person1", 20)); - _source.AddOrUpdate(new Person("Person1", 21)); subscriber.Dispose(); - called.Should().BeTrue(); } - [Fact] - public void Remove() + public void Dispose() { - bool called = false; - IDisposable subscriber = _source.Connect().Group(p => p.Age) - .Skip(1) - .Subscribe( - updates => - { - updates.Count.Should().Be(1, "Should be 1 add"); - updates.First().Reason.Should().Be(ChangeReason.Remove); - called = true; - }); - _source.AddOrUpdate(new Person("Person1", 20)); - _source.Remove(new Person("Person1", 20)); - subscriber.Dispose(); - called.Should().BeTrue(); + _source.Dispose(); } [Fact] public void FiresCompletedWhenDisposed() { bool completed = false; - IDisposable subscriber = _source.Connect().Group(p => p.Age) - .Subscribe(updates => { }, - () => { completed = true; }); + IDisposable subscriber = _source.Connect().Group(p => p.Age).Subscribe(updates => { }, () => { completed = true; }); _source.Dispose(); subscriber.Dispose(); completed.Should().BeTrue(); @@ -100,25 +80,25 @@ public void FiresCompletedWhenDisposed() public void FiresManyValueForBatchOfDifferentAdds() { bool called = false; - IDisposable subscriber = _source.Connect().Group(p => p.Age) - .Subscribe( - updates => - { - updates.Count.Should().Be(4, "Should be 4 adds"); - foreach (var update in updates) - { - update.Reason.Should().Be(ChangeReason.Add); - } - - called = true; - }); - _source.Edit(updater => - { - updater.AddOrUpdate(new Person("Person1", 20)); - updater.AddOrUpdate(new Person("Person2", 21)); - updater.AddOrUpdate(new Person("Person3", 22)); - updater.AddOrUpdate(new Person("Person4", 23)); - }); + IDisposable subscriber = _source.Connect().Group(p => p.Age).Subscribe( + updates => + { + updates.Count.Should().Be(4, "Should be 4 adds"); + foreach (var update in updates) + { + update.Reason.Should().Be(ChangeReason.Add); + } + + called = true; + }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person("Person1", 20)); + updater.AddOrUpdate(new Person("Person2", 21)); + updater.AddOrUpdate(new Person("Person3", 22)); + updater.AddOrUpdate(new Person("Person4", 23)); + }); subscriber.Dispose(); called.Should().BeTrue(); @@ -128,21 +108,21 @@ public void FiresManyValueForBatchOfDifferentAdds() public void FiresOnlyOnceForABatchOfUniqueValues() { bool called = false; - IDisposable subscriber = _source.Connect().Group(p => p.Age) - .Subscribe( - updates => - { - updates.Count.Should().Be(1, "Should be 1 add"); - updates.First().Reason.Should().Be(ChangeReason.Add); - called = true; - }); - _source.Edit(updater => - { - updater.AddOrUpdate(new Person("Person1", 20)); - updater.AddOrUpdate(new Person("Person2", 20)); - updater.AddOrUpdate(new Person("Person3", 20)); - updater.AddOrUpdate(new Person("Person4", 20)); - }); + IDisposable subscriber = _source.Connect().Group(p => p.Age).Subscribe( + updates => + { + updates.Count.Should().Be(1, "Should be 1 add"); + updates.First().Reason.Should().Be(ChangeReason.Add); + called = true; + }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person("Person1", 20)); + updater.AddOrUpdate(new Person("Person2", 20)); + updater.AddOrUpdate(new Person("Person3", 20)); + updater.AddOrUpdate(new Person("Person4", 20)); + }); subscriber.Dispose(); called.Should().BeTrue(); @@ -153,18 +133,17 @@ public void FiresRemoveWhenEmptied() { bool called = false; //skip first one a this is setting up the stream - IDisposable subscriber = _source.Connect().Group(p => p.Age).Skip(1) - .Subscribe( - updates => - { - updates.Count.Should().Be(1, "Should be 1 update"); - foreach (var update in updates) - { - update.Reason.Should().Be(ChangeReason.Remove); - } - - called = true; - }); + IDisposable subscriber = _source.Connect().Group(p => p.Age).Skip(1).Subscribe( + updates => + { + updates.Count.Should().Be(1, "Should be 1 update"); + foreach (var update in updates) + { + update.Reason.Should().Be(ChangeReason.Remove); + } + + called = true; + }); var person = new Person("Person1", 20); _source.AddOrUpdate(person); @@ -180,93 +159,93 @@ public void FiresRemoveWhenEmptied() public void ReceivesUpdateWhenFeederIsInvoked() { bool called = false; - var subscriber = _source.Connect().Group(p => p.Age) - .Subscribe(updates => { called = true; }); + var subscriber = _source.Connect().Group(p => p.Age).Subscribe(updates => { called = true; }); _source.AddOrUpdate(new Person("Person1", 20)); subscriber.Dispose(); called.Should().BeTrue(); - } - - [Fact] - public void AddItemAfterUpdateItemProcessAdd() - { - var subscriber = _source.Connect() - .Group(x => x.Name[0].ToString()) - .Transform(x => new GroupViewModel(x)) - .Bind(out _entries) - .Subscribe(); - - _source.Edit(x => - { - x.AddOrUpdate(new Person("Adam", 1)); - }); - - var firstGroup = _entries.First(); - firstGroup.Entries.Count.Should().Be(1); - - _source.Edit(x => - { - x.AddOrUpdate(new Person("Adam", 3)); // update - x.AddOrUpdate(new Person("Alfred", 1)); // add - }); - - firstGroup.Entries.Count.Should().Be(2); - - subscriber.Dispose(); - } - - [Fact] - public void UpdateItemAfterAddItemProcessAdd() - { - var subscriber = _source.Connect() - .Group(x => x.Name[0].ToString()) - .Transform(x => new GroupViewModel(x)) - .Bind(out _entries) - .Subscribe(); - - _source.Edit(x => - { - x.AddOrUpdate(new Person("Adam", 1)); - }); - - var firstGroup = _entries.First(); - firstGroup.Entries.Count.Should().Be(1); - - _source.Edit(x => - { - x.AddOrUpdate(new Person("Alfred", 1)); // add - x.AddOrUpdate(new Person("Adam", 3)); // update - }); - - firstGroup.Entries.Count.Should().Be(2); - - subscriber.Dispose(); - } - - private ReadOnlyObservableCollection _entries; - - public class GroupViewModel - { - public ReadOnlyObservableCollection Entries => _entries; - private readonly ReadOnlyObservableCollection _entries; - - public GroupViewModel(IGroup person) - { - person.Cache.Connect() - .Transform(x => new GroupEntryViewModel(x)) - .Bind(out _entries) - .Subscribe(); - } - } - - public class GroupEntryViewModel - { - public Person Person { get; } - - public GroupEntryViewModel(Person person) - { - Person = person; - } - } - } -} + } + + [Fact] + public void Remove() + { + bool called = false; + IDisposable subscriber = _source.Connect().Group(p => p.Age).Skip(1).Subscribe( + updates => + { + updates.Count.Should().Be(1, "Should be 1 add"); + updates.First().Reason.Should().Be(ChangeReason.Remove); + called = true; + }); + _source.AddOrUpdate(new Person("Person1", 20)); + _source.Remove(new Person("Person1", 20)); + subscriber.Dispose(); + called.Should().BeTrue(); + } + + [Fact] + public void UpdateAnItemWillChangedThegroup() + { + bool called = false; + IDisposable subscriber = _source.Connect().Group(p => p.Age).Subscribe(updates => { called = true; }); + _source.AddOrUpdate(new Person("Person1", 20)); + _source.AddOrUpdate(new Person("Person1", 21)); + subscriber.Dispose(); + called.Should().BeTrue(); + } + + [Fact] + public void UpdateItemAfterAddItemProcessAdd() + { + var subscriber = _source.Connect().Group(x => x.Name[0].ToString()).Transform(x => new GroupViewModel(x)).Bind(out _entries).Subscribe(); + + _source.Edit(x => { x.AddOrUpdate(new Person("Adam", 1)); }); + + var firstGroup = _entries.First(); + firstGroup.Entries.Count.Should().Be(1); + + _source.Edit( + x => + { + x.AddOrUpdate(new Person("Alfred", 1)); // add + x.AddOrUpdate(new Person("Adam", 3)); // update + }); + + firstGroup.Entries.Count.Should().Be(2); + + subscriber.Dispose(); + } + + [Fact] + public void UpdateNotPossible() + { + bool called = false; + IDisposable subscriber = _source.Connect().Group(p => p.Age).Skip(1).Subscribe(updates => { called = true; }); + _source.AddOrUpdate(new Person("Person1", 20)); + _source.AddOrUpdate(new Person("Person1", 20)); + subscriber.Dispose(); + called.Should().BeFalse(); + } + + public class GroupEntryViewModel + { + public GroupEntryViewModel(Person person) + { + Person = person; + } + + public Person Person { get; } + } + + public class GroupViewModel + { + private readonly ReadOnlyObservableCollection _entries; + + public GroupViewModel(IGroup person) + { + person.Cache.Connect().Transform(x => new GroupEntryViewModel(x)).Bind(out _entries).Subscribe(); + } + + public ReadOnlyObservableCollection Entries => _entries; + } + } +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/GroupFromDistinctFixture.cs b/src/DynamicData.Tests/Cache/GroupFromDistinctFixture.cs index 2af059eb7..f4493b818 100644 --- a/src/DynamicData.Tests/Cache/GroupFromDistinctFixture.cs +++ b/src/DynamicData.Tests/Cache/GroupFromDistinctFixture.cs @@ -1,19 +1,22 @@ using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class GroupFromDistinctFixture : IDisposable { - private readonly ISourceCache _personCache; private readonly ISourceCache _employmentCache; + private readonly ISourceCache _personCache; + public GroupFromDistinctFixture() { _personCache = new SourceCache(p => p.Key); @@ -31,23 +34,21 @@ public void GroupFromDistinct() { const int numberOfPeople = 1000; var random = new Random(); - var companies = new List {"Company A", "Company B", "Company C"}; + var companies = new List { "Company A", "Company B", "Company C" }; //create 100 people var people = Enumerable.Range(1, numberOfPeople).Select(i => new Person($"Person{i}", i)).ToArray(); //create 0-3 jobs for each person and select from companies - var emphistory = Enumerable.Range(1, numberOfPeople).SelectMany(i => - { - var companiestogenrate = random.Next(0, 4); - return Enumerable.Range(0, companiestogenrate).Select(c => new PersonEmployment($"Person{i}", companies[c])); - }).ToList(); + var emphistory = Enumerable.Range(1, numberOfPeople).SelectMany( + i => + { + var companiestogenrate = random.Next(0, 4); + return Enumerable.Range(0, companiestogenrate).Select(c => new PersonEmployment($"Person{i}", companies[c])); + }).ToList(); // Cache results - var allpeopleWithEmpHistory = _employmentCache.Connect() - .Group(e => e.Name, _personCache.Connect().DistinctValues(p => p.Name)) - .Transform(x => new PersonWithEmployment(x)) - .AsObservableCache(); + var allpeopleWithEmpHistory = _employmentCache.Connect().Group(e => e.Name, _personCache.Connect().DistinctValues(p => p.Name)).Transform(x => new PersonWithEmployment(x)).AsObservableCache(); _personCache.AddOrUpdate(people); _employmentCache.AddOrUpdate(emphistory); @@ -56,10 +57,7 @@ public void GroupFromDistinct() allpeopleWithEmpHistory.Items.SelectMany(d => d.EmploymentData.Items).Count().Should().Be(emphistory.Count); //check grouped items have the same key as the parent - allpeopleWithEmpHistory.Items.ForEach - ( - p => { p.EmploymentData.Items.All(emph => emph.Name == p.Person).Should().BeTrue(); } - ); + allpeopleWithEmpHistory.Items.ForEach(p => { p.EmploymentData.Items.All(emph => emph.Name == p.Person).Should().BeTrue(); }); _personCache.Edit(updater => updater.Remove("Person1")); allpeopleWithEmpHistory.Count.Should().Be(numberOfPeople - 1); @@ -67,4 +65,4 @@ public void GroupFromDistinct() allpeopleWithEmpHistory.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/GroupImmutableFixture.cs b/src/DynamicData.Tests/Cache/GroupImmutableFixture.cs index be9ba6f3c..af1f5ca39 100644 --- a/src/DynamicData.Tests/Cache/GroupImmutableFixture.cs +++ b/src/DynamicData.Tests/Cache/GroupImmutableFixture.cs @@ -1,89 +1,83 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class GroupImmutableFixture: IDisposable + public class GroupImmutableFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator, int> _results; - public GroupImmutableFixture() + private readonly ISourceCache _source; + + public GroupImmutableFixture() { _source = new SourceCache(p => p.Name); _results = _source.Connect().GroupWithImmutableState(p => p.Age).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void Add() { - _source.AddOrUpdate(new Person("Person1", 20)); _results.Data.Count.Should().Be(1, "Should be 1 add"); _results.Messages.First().Adds.Should().Be(1); } [Fact] - public void UpdatesArePermissible() + public void ChanegMultipleGroups() { - _source.AddOrUpdate(new Person("Person1", 20)); - _source.AddOrUpdate(new Person("Person2", 20)); + var initialPeople = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i % 10)).ToArray(); - _results.Data.Count.Should().Be(1);//1 group - _results.Messages.First().Adds.Should().Be(1); - _results.Messages.Skip(1).First().Updates.Should().Be(1); + _source.AddOrUpdate(initialPeople); - var group = _results.Data.Items.First(); - group.Count.Should().Be(2); - } + initialPeople.GroupBy(p => p.Age).ForEach( + group => + { + var cache = _results.Data.Lookup(group.Key).Value; + cache.Items.Should().BeEquivalentTo(group); + }); - [Fact] - public void UpdateAnItemWillChangedThegroup() - { - _source.AddOrUpdate(new Person("Person1", 20)); - _source.AddOrUpdate(new Person("Person1", 21)); + var changedPeople = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i % 5)).ToArray(); - _results.Data.Count.Should().Be(1); - _results.Messages.First().Adds.Should().Be(1); - _results.Messages.Skip(1).First().Adds.Should().Be(1); - _results.Messages.Skip(1).First().Removes.Should().Be(1); - var group = _results.Data.Items.First(); - group.Count.Should().Be(1); + _source.AddOrUpdate(changedPeople); - group.Key.Should().Be(21); + changedPeople.GroupBy(p => p.Age).ForEach( + group => + { + var cache = _results.Data.Lookup(group.Key).Value; + cache.Items.Should().BeEquivalentTo(group); + }); + + _results.Messages.Count.Should().Be(2); + _results.Messages.First().Adds.Should().Be(10); + _results.Messages.Skip(1).First().Removes.Should().Be(5); + _results.Messages.Skip(1).First().Updates.Should().Be(5); } - [Fact] - public void Remove() + public void Dispose() { - _source.AddOrUpdate(new Person("Person1", 20)); - _source.Remove(new Person("Person1", 20)); - - _results.Messages.Count.Should().Be(2); - _results.Data.Count.Should().Be(0); + _source.Dispose(); + _results.Dispose(); } [Fact] public void FiresManyValueForBatchOfDifferentAdds() { - _source.Edit(updater => - { - updater.AddOrUpdate(new Person("Person1", 20)); - updater.AddOrUpdate(new Person("Person2", 21)); - updater.AddOrUpdate(new Person("Person3", 22)); - updater.AddOrUpdate(new Person("Person4", 23)); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person("Person1", 20)); + updater.AddOrUpdate(new Person("Person2", 21)); + updater.AddOrUpdate(new Person("Person3", 22)); + updater.AddOrUpdate(new Person("Person4", 23)); + }); _results.Data.Count.Should().Be(4); _results.Messages.Count.Should().Be(1); @@ -97,60 +91,24 @@ public void FiresManyValueForBatchOfDifferentAdds() [Fact] public void FiresOnlyOnceForABatchOfUniqueValues() { - _source.Edit(updater => - { - updater.AddOrUpdate(new Person("Person1", 20)); - updater.AddOrUpdate(new Person("Person2", 20)); - updater.AddOrUpdate(new Person("Person3", 20)); - updater.AddOrUpdate(new Person("Person4", 20)); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person("Person1", 20)); + updater.AddOrUpdate(new Person("Person2", 20)); + updater.AddOrUpdate(new Person("Person3", 20)); + updater.AddOrUpdate(new Person("Person4", 20)); + }); _results.Messages.Count.Should().Be(1); _results.Messages.First().Adds.Should().Be(1); _results.Data.Items.First().Count.Should().Be(4); } - [Fact] - public void ChanegMultipleGroups() - { - var initialPeople = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, i % 10)) - .ToArray(); - - _source.AddOrUpdate(initialPeople); - - initialPeople.GroupBy(p => p.Age) - .ForEach(group => - { - var cache = _results.Data.Lookup(group.Key).Value; - cache.Items.Should().BeEquivalentTo(group); - }); - - var changedPeople = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, i % 5)) - .ToArray(); - - _source.AddOrUpdate(changedPeople); - - changedPeople.GroupBy(p => p.Age) - .ForEach(group => - { - var cache = _results.Data.Lookup(group.Key).Value; - cache.Items.Should().BeEquivalentTo(group); - }); - - _results.Messages.Count.Should().Be(2); - _results.Messages.First().Adds.Should().Be(10); - _results.Messages.Skip(1).First().Removes.Should().Be(5); - _results.Messages.Skip(1).First().Updates.Should().Be(5); - } - [Fact] public void Reevaluate() { - var initialPeople = Enumerable.Range(1, 10) - .Select(i => new Person("Person" + i, i % 2)) - .ToArray(); + var initialPeople = Enumerable.Range(1, 10).Select(i => new Person("Person" + i, i % 2)).ToArray(); _source.AddOrUpdate(initialPeople); _results.Messages.Count.Should().Be(1); @@ -158,19 +116,18 @@ public void Reevaluate() //do an inline update foreach (var person in initialPeople) { - person.Age = person.Age + 1; + person.Age += 1; } //signal operators to evaluate again _source.Refresh(); - initialPeople.GroupBy(p => p.Age) - .ForEach(group => - { - var cache = _results.Data.Lookup(group.Key).Value; - cache.Items.Should().BeEquivalentTo(group); - - }); + initialPeople.GroupBy(p => p.Age).ForEach( + group => + { + var cache = _results.Data.Lookup(group.Key).Value; + cache.Items.Should().BeEquivalentTo(group); + }); _results.Data.Count.Should().Be(2); _results.Messages.Count.Should().Be(2); @@ -180,5 +137,45 @@ public void Reevaluate() secondMessage.Updates.Should().Be(1); secondMessage.Adds.Should().Be(1); } + + [Fact] + public void Remove() + { + _source.AddOrUpdate(new Person("Person1", 20)); + _source.Remove(new Person("Person1", 20)); + + _results.Messages.Count.Should().Be(2); + _results.Data.Count.Should().Be(0); + } + + [Fact] + public void UpdateAnItemWillChangedThegroup() + { + _source.AddOrUpdate(new Person("Person1", 20)); + _source.AddOrUpdate(new Person("Person1", 21)); + + _results.Data.Count.Should().Be(1); + _results.Messages.First().Adds.Should().Be(1); + _results.Messages.Skip(1).First().Adds.Should().Be(1); + _results.Messages.Skip(1).First().Removes.Should().Be(1); + var group = _results.Data.Items.First(); + group.Count.Should().Be(1); + + group.Key.Should().Be(21); + } + + [Fact] + public void UpdatesArePermissible() + { + _source.AddOrUpdate(new Person("Person1", 20)); + _source.AddOrUpdate(new Person("Person2", 20)); + + _results.Data.Count.Should().Be(1); //1 group + _results.Messages.First().Adds.Should().Be(1); + _results.Messages.Skip(1).First().Updates.Should().Be(1); + + var group = _results.Data.Items.First(); + group.Count.Should().Be(2); + } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/GroupOnPropertyFixture.cs b/src/DynamicData.Tests/Cache/GroupOnPropertyFixture.cs index 6161397a2..2c025ab0e 100644 --- a/src/DynamicData.Tests/Cache/GroupOnPropertyFixture.cs +++ b/src/DynamicData.Tests/Cache/GroupOnPropertyFixture.cs @@ -1,28 +1,25 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class GroupOnPropertyFixture: IDisposable + public class GroupOnPropertyFixture : IDisposable { - private readonly SourceCache _source; private readonly ChangeSetAggregator, int> _results; - public GroupOnPropertyFixture() - { - _source = new SourceCache(p=>p.Key); - _results = _source.Connect().GroupOnProperty(p => p.Age).AsAggregator(); - } + private readonly SourceCache _source; - public void Dispose() + public GroupOnPropertyFixture() { - _source.Dispose(); - _results.Dispose(); + _source = new SourceCache(p => p.Key); + _results = _source.Connect().GroupOnProperty(p => p.Age).AsAggregator(); } [Fact] @@ -38,30 +35,6 @@ public void CanGroupOnAdds() firstGroup.Key.Should().Be(10); } - [Fact] - public void CanRemoveFromGroup() - { - var person = new Person("A", 10); - _source.AddOrUpdate(person); - _source.Remove(person); - - _results.Data.Count.Should().Be(0); - } - - [Fact] - public void Regroup() - { - var person = new Person("A", 10); - _source.AddOrUpdate(person); - person.Age = 20; - - _results.Data.Count.Should().Be(1); - var firstGroup = _results.Data.Items.First(); - - firstGroup.Cache.Count.Should().Be(1); - firstGroup.Key.Should().Be(20); - } - [Fact] public void CanHandleAddBatch() { @@ -85,19 +58,45 @@ public void CanHandleChangedItemsBatch() var initialCount = people.Select(p => p.Age).Distinct().Count(); _results.Data.Count.Should().Be(initialCount); - people.Take(25) - .ForEach(p => p.Age = 200); + people.Take(25).ForEach(p => p.Age = 200); var changedCount = people.Select(p => p.Age).Distinct().Count(); _results.Data.Count.Should().Be(changedCount); //check that each item is only in one cache - var peopleInCache = _results.Data.Items - .SelectMany(g => g.Cache.Items) - .ToArray(); + var peopleInCache = _results.Data.Items.SelectMany(g => g.Cache.Items).ToArray(); peopleInCache.Length.Should().Be(100); + } + [Fact] + public void CanRemoveFromGroup() + { + var person = new Person("A", 10); + _source.AddOrUpdate(person); + _source.Remove(person); + + _results.Data.Count.Should().Be(0); + } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void Regroup() + { + var person = new Person("A", 10); + _source.AddOrUpdate(person); + person.Age = 20; + + _results.Data.Count.Should().Be(1); + var firstGroup = _results.Data.Items.First(); + + firstGroup.Cache.Count.Should().Be(1); + firstGroup.Key.Should().Be(20); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/GroupOnPropertyWithImmutableStateFixture.cs b/src/DynamicData.Tests/Cache/GroupOnPropertyWithImmutableStateFixture.cs index ef8a0e8da..9b7cc56f5 100644 --- a/src/DynamicData.Tests/Cache/GroupOnPropertyWithImmutableStateFixture.cs +++ b/src/DynamicData.Tests/Cache/GroupOnPropertyWithImmutableStateFixture.cs @@ -1,30 +1,27 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class GroupOnPropertyWithImmutableStateFixture: IDisposable + public class GroupOnPropertyWithImmutableStateFixture : IDisposable { - private readonly SourceCache _source; private readonly ChangeSetAggregator, int> _results; + private readonly SourceCache _source; + public GroupOnPropertyWithImmutableStateFixture() { _source = new SourceCache(p => p.Key); _results = _source.Connect().GroupOnPropertyWithImmutableState(p => p.Age).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void CanGroupOnAdds() { @@ -38,30 +35,6 @@ public void CanGroupOnAdds() firstGroup.Key.Should().Be(10); } - [Fact] - public void CanRemoveFromGroup() - { - var person = new Person("A", 10); - _source.AddOrUpdate(person); - _source.Remove(person); - - _results.Data.Count.Should().Be(0); - } - - [Fact] - public void Regroup() - { - var person = new Person("A", 10); - _source.AddOrUpdate(person); - person.Age = 20; - - _results.Data.Count.Should().Be(1); - var firstGroup = _results.Data.Items.First(); - - firstGroup.Count.Should().Be(1); - firstGroup.Key.Should().Be(20); - } - [Fact] public void CanHandleAddBatch() { @@ -85,19 +58,45 @@ public void CanHandleChangedItemsBatch() var initialCount = people.Select(p => p.Age).Distinct().Count(); _results.Data.Count.Should().Be(initialCount); - people.Take(25) - .ForEach(p => p.Age = 200); + people.Take(25).ForEach(p => p.Age = 200); var changedCount = people.Select(p => p.Age).Distinct().Count(); _results.Data.Count.Should().Be(changedCount); //check that each item is only in one cache - var peopleInCache = _results.Data.Items - .SelectMany(g => g.Items) - .ToArray(); + var peopleInCache = _results.Data.Items.SelectMany(g => g.Items).ToArray(); peopleInCache.Length.Should().Be(100); + } + [Fact] + public void CanRemoveFromGroup() + { + var person = new Person("A", 10); + _source.AddOrUpdate(person); + _source.Remove(person); + + _results.Data.Count.Should().Be(0); + } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void Regroup() + { + var person = new Person("A", 10); + _source.AddOrUpdate(person); + person.Age = 20; + + _results.Data.Count.Should().Be(1); + var firstGroup = _results.Data.Items.First(); + + firstGroup.Count.Should().Be(1); + firstGroup.Key.Should().Be(20); } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/IgnoreUpdateFixture.cs b/src/DynamicData.Tests/Cache/IgnoreUpdateFixture.cs index 4780c0e92..7736fc29c 100644 --- a/src/DynamicData.Tests/Cache/IgnoreUpdateFixture.cs +++ b/src/DynamicData.Tests/Cache/IgnoreUpdateFixture.cs @@ -1,23 +1,28 @@ using System; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class IgnoreUpdateFixture: IDisposable + public class IgnoreUpdateFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; - public IgnoreUpdateFixture() + private readonly ISourceCache _source; + + public IgnoreUpdateFixture() { _source = new SourceCache(p => p.Key); - _results = new ChangeSetAggregator - ( - _source.Connect().IgnoreUpdateWhen((current, previous) => current == previous) - ); + _results = new ChangeSetAggregator(_source.Connect().IgnoreUpdateWhen((current, previous) => current == previous)); + } + + public void Dispose() + { + _source.Dispose(); } [Fact] @@ -31,10 +36,5 @@ public void IgnoreFunctionWillIgnoreSubsequentUpdatesOfAnItem() _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); } - - public void Dispose() - { - _source.Dispose(); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/IncludeUpdateFixture.cs b/src/DynamicData.Tests/Cache/IncludeUpdateFixture.cs index 9099eacae..693e1d9c9 100644 --- a/src/DynamicData.Tests/Cache/IncludeUpdateFixture.cs +++ b/src/DynamicData.Tests/Cache/IncludeUpdateFixture.cs @@ -1,23 +1,28 @@ using System; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class IncludeUpdateFixture: IDisposable + public class IncludeUpdateFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; - public IncludeUpdateFixture() + private readonly ISourceCache _source; + + public IncludeUpdateFixture() { _source = new SourceCache(p => p.Key); - _results = new ChangeSetAggregator - ( - _source.Connect().IncludeUpdateWhen((current, previous) => current != previous) - ); + _results = new ChangeSetAggregator(_source.Connect().IncludeUpdateWhen((current, previous) => current != previous)); + } + + public void Dispose() + { + _source.Dispose(); } [Fact] @@ -31,10 +36,5 @@ public void IgnoreFunctionWillIgnoreSubsequentUpdatesOfAnItem() _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); } - - public void Dispose() - { - _source.Dispose(); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/InnerJoinFixture.cs b/src/DynamicData.Tests/Cache/InnerJoinFixture.cs index cba47dcd2..6d684a47c 100644 --- a/src/DynamicData.Tests/Cache/InnerJoinFixture.cs +++ b/src/DynamicData.Tests/Cache/InnerJoinFixture.cs @@ -1,44 +1,40 @@ using System; using System.Linq; -using System.Reactive; + using DynamicData.Kernel; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - public class InnerJoinFixture: IDisposable + public class InnerJoinFixture : IDisposable { - private SourceCache _left; - private SourceCache _right; - private ChangeSetAggregator _result; + private readonly SourceCache _left; + + private readonly ChangeSetAggregator _result; + + private readonly SourceCache _right; public InnerJoinFixture() { _left = new SourceCache(device => device.Name); _right = new SourceCache(device => device.Name); - _result = _left.Connect() - .InnerJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(key, device, meta)) - .AsAggregator(); - } - - public void Dispose() - { - _left?.Dispose(); - _right?.Dispose(); - _result?.Dispose(); + _result = _left.Connect().InnerJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(key, device, meta)).AsAggregator(); } [Fact] public void AddLeftOnly() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 0.Should().Be(_result.Data.Count); _result.Data.Lookup("Device1").HasValue.Should().BeFalse(); @@ -46,15 +42,40 @@ public void AddLeftOnly() _result.Data.Lookup("Device3").HasValue.Should().BeFalse(); } + [Fact] + public void AddLetThenRight() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + 3.Should().Be(_result.Data.Count); + + _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); + } + [Fact] public void AddRightOnly() { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); 0.Should().Be(_result.Data.Count); _result.Data.Lookup("Device1").HasValue.Should().BeFalse(); @@ -63,43 +84,52 @@ public void AddRightOnly() } [Fact] - public void AddLetThenRight() + public void AddRightThenLeft() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); + } - _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); + public void Dispose() + { + _left?.Dispose(); + _right?.Dispose(); + _result?.Dispose(); } [Fact] public void RemoveVarious() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); _result.Data.Lookup("Device1").HasValue.Should().BeTrue(); _result.Data.Lookup("Device2").HasValue.Should().BeTrue(); @@ -116,58 +146,48 @@ public void RemoveVarious() _result.Data.Lookup("Device3").HasValue.Should().BeFalse(); } - [Fact] - public void AddRightThenLeft() - { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); - - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - 3.Should().Be(_result.Data.Count); - } - [Fact] public void UpdateRight() { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); - - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); } public class Device : IEquatable { - public string Name { get; } - public Device(string name) { Name = name; } - #region Equality Members + public string Name { get; } - public bool Equals(Device other) + public static bool operator ==(Device left, Device right) + { + return Equals(left, right); + } + + public static bool operator !=(Device left, Device right) + { + return !Equals(left, right); + } + + public bool Equals(Device? other) { if (ReferenceEquals(null, other)) { @@ -182,7 +202,7 @@ public bool Equals(Device other) return string.Equals(Name, other.Name); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -204,21 +224,9 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Name != null ? Name.GetHashCode() : 0); - } - - public static bool operator ==(Device left, Device right) - { - return Equals(left, right); + return (Name is not null ? Name.GetHashCode() : 0); } - public static bool operator !=(Device left, Device right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{Name}"; @@ -227,19 +235,27 @@ public override string ToString() public class DeviceMetaData : IEquatable { - public string Name { get; } - - public bool IsAutoConnect { get; } - public DeviceMetaData(string name, bool isAutoConnect = false) { Name = name; IsAutoConnect = isAutoConnect; } - #region Equality members + public bool IsAutoConnect { get; } + + public string Name { get; } + + public static bool operator ==(DeviceMetaData left, DeviceMetaData right) + { + return Equals(left, right); + } + + public static bool operator !=(DeviceMetaData left, DeviceMetaData right) + { + return !Equals(left, right); + } - public bool Equals(DeviceMetaData other) + public bool Equals(DeviceMetaData? other) { if (ReferenceEquals(null, other)) { @@ -254,7 +270,7 @@ public bool Equals(DeviceMetaData other) return string.Equals(Name, other.Name) && IsAutoConnect == other.IsAutoConnect; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -278,22 +294,10 @@ public override int GetHashCode() { unchecked { - return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ IsAutoConnect.GetHashCode(); + return ((Name is not null ? Name.GetHashCode() : 0) * 397) ^ IsAutoConnect.GetHashCode(); } } - public static bool operator ==(DeviceMetaData left, DeviceMetaData right) - { - return Equals(left, right); - } - - public static bool operator !=(DeviceMetaData left, DeviceMetaData right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"Metadata: {Name}. IsAutoConnect = {IsAutoConnect}"; @@ -302,10 +306,6 @@ public override string ToString() public class DeviceWithMetadata : IEquatable { - public string Key { get; } - public Device Device { get; set; } - public DeviceMetaData MetaData { get; } - public DeviceWithMetadata(string key, Device device, DeviceMetaData metaData) { Key = key; @@ -313,9 +313,23 @@ public DeviceWithMetadata(string key, Device device, DeviceMetaData metaData) MetaData = metaData; } - #region Equality members + public Device Device { get; set; } + + public string Key { get; } + + public DeviceMetaData MetaData { get; } + + public static bool operator ==(DeviceWithMetadata left, DeviceWithMetadata right) + { + return Equals(left, right); + } + + public static bool operator !=(DeviceWithMetadata left, DeviceWithMetadata right) + { + return !Equals(left, right); + } - public bool Equals(DeviceWithMetadata other) + public bool Equals(DeviceWithMetadata? other) { if (ReferenceEquals(null, other)) { @@ -330,7 +344,7 @@ public bool Equals(DeviceWithMetadata other) return string.Equals(Key, other.Key); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -352,26 +366,13 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Key != null ? Key.GetHashCode() : 0); + return (Key is not null ? Key.GetHashCode() : 0); } - public static bool operator ==(DeviceWithMetadata left, DeviceWithMetadata right) - { - return Equals(left, right); - } - - public static bool operator !=(DeviceWithMetadata left, DeviceWithMetadata right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{Key}: {Device} ({MetaData})"; } } - } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/InnerJoinFixtureRaceCondition.cs b/src/DynamicData.Tests/Cache/InnerJoinFixtureRaceCondition.cs index 9d0e26938..7b56eaa77 100644 --- a/src/DynamicData.Tests/Cache/InnerJoinFixtureRaceCondition.cs +++ b/src/DynamicData.Tests/Cache/InnerJoinFixtureRaceCondition.cs @@ -1,18 +1,13 @@ using System; using System.Reactive.Concurrency; using System.Reactive.Linq; + using Xunit; namespace DynamicData.Tests.Cache { public class InnerJoinFixtureRaceCondition { - public class Thing - { - public long Id { get; set; } - public string Name { get; set; } - } - /// /// Tests to see whether we have fixed a race condition. See https://github.com/reactiveui/DynamicData/issues/364 /// @@ -22,23 +17,26 @@ public class Thing [Fact] public void LetsSeeWhetherWeCanRandomlyHitARaceCondition() { - var ids = ObservableChangeSet.Create(sourceCache => - { - return Observable.Range(1, 1000000, Scheduler.Default) - .Subscribe(x => sourceCache.AddOrUpdate(x)); - }, x => x); + var ids = ObservableChangeSet.Create(sourceCache => { return Observable.Range(1, 1000000, Scheduler.Default).Subscribe(x => sourceCache.AddOrUpdate(x)); }, x => x); var itemsCache = new SourceCache(x => x.Id); - itemsCache.AddOrUpdate(new[] - { - new Thing {Id = 300, Name = "Quick"}, - new Thing {Id = 600, Name = "Brown"}, - new Thing {Id = 900, Name = "Fox"}, - new Thing {Id = 1200, Name = "Hello"}, - }); + itemsCache.AddOrUpdate( + new[] + { + new Thing { Id = 300, Name = "Quick" }, + new Thing { Id = 600, Name = "Brown" }, + new Thing { Id = 900, Name = "Fox" }, + new Thing { Id = 1200, Name = "Hello" }, + }); + + ids.InnerJoin(itemsCache.Connect(), x => x.Id, (_, thing) => thing).Subscribe((z) => { }, ex => { }, () => { }); + } + + public class Thing + { + public long Id { get; set; } - ids.InnerJoin(itemsCache.Connect(), x => x.Id, (_, thing) => thing) - .Subscribe((z)=>{},ex=>{},()=>{}); + public string? Name { get; set; } } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/InnerJoinManyFixture.cs b/src/DynamicData.Tests/Cache/InnerJoinManyFixture.cs index d902cafee..dff8fae52 100644 --- a/src/DynamicData.Tests/Cache/InnerJoinManyFixture.cs +++ b/src/DynamicData.Tests/Cache/InnerJoinManyFixture.cs @@ -1,38 +1,52 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class InnerJoinManyFixture: IDisposable + public class InnerJoinManyFixture : IDisposable { private readonly SourceCache _people; + private readonly ChangeSetAggregator _result; - public InnerJoinManyFixture() + public InnerJoinManyFixture() { _people = new SourceCache(p => p.Name); //Only parent which have children will be included - _result = _people.Connect() - .InnerJoinMany( _people.Connect(), pac => pac.ParentName, (person, grouping) => new ParentAndChildren(person, grouping.Items.Select(p => p).ToArray())) - .AsAggregator(); + _result = _people.Connect().InnerJoinMany(_people.Connect(), pac => pac.ParentName, (person, grouping) => new ParentAndChildren(person, grouping.Items.Select(p => p).ToArray())).AsAggregator(); } - public void Dispose() + [Fact] + public void AddChild() { - _people.Dispose(); - _result.Dispose(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); + + _people.AddOrUpdate(people); + + var person11 = new Person("Person11", 100, parentName: "Person3"); + _people.AddOrUpdate(person11); + + var updatedPeople = people.Union(new[] { person11 }).ToArray(); + + AssertDataIsCorrectlyFormed(updatedPeople); } [Fact] public void AddLeftOnly() { - var people = Enumerable.Range(1, 10) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + var people = Enumerable.Range(1, 10).Select(i => new Person("Person" + i, i)).ToArray(); _people.AddOrUpdate(people); @@ -42,51 +56,53 @@ public void AddLeftOnly() [Fact] public void AddPeopleWithParents() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); AssertDataIsCorrectlyFormed(people); } + public void Dispose() + { + _people.Dispose(); + _result.Dispose(); + } + [Fact] - public void UpdateParent() + public void RemoveChild() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); - var current10 = people.Last(); - var person10 = new Person("Person10", 100, parentName: current10.ParentName); - _people.AddOrUpdate(person10); + var last = people.Last(); + _people.Remove(last); - var updatedPeople = people.Take(9).Union(new[] { person10 }).ToArray(); + var updatedPeople = people.Where(p => p.Name != last.Name).ToArray(); - AssertDataIsCorrectlyFormed(updatedPeople); + AssertDataIsCorrectlyFormed(updatedPeople, last.Name); } [Fact] public void UpdateChild() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); @@ -100,68 +116,45 @@ public void UpdateChild() } [Fact] - public void AddChild() + public void UpdateParent() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); - var person11 = new Person("Person11", 100, parentName: "Person3"); - _people.AddOrUpdate(person11); + var current10 = people.Last(); + var person10 = new Person("Person10", 100, parentName: current10.ParentName); + _people.AddOrUpdate(person10); - var updatedPeople = people.Union(new[] { person11 }).ToArray(); + var updatedPeople = people.Take(9).Union(new[] { person10 }).ToArray(); AssertDataIsCorrectlyFormed(updatedPeople); } - [Fact] - public void RemoveChild() - { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); - - _people.AddOrUpdate(people); - - var last = people.Last(); - _people.Remove(last); - - var updatedPeople = people.Where(p => p.Name != last.Name).ToArray(); - - AssertDataIsCorrectlyFormed(updatedPeople, last.Name); - } - private void AssertDataIsCorrectlyFormed(Person[] allPeople, params string[] missingParents) { - var grouped = allPeople.GroupBy(p => p.ParentName ) - .Where(p => p.Any() && !missingParents.Contains(p.Key)) - .AsArray(); + var grouped = allPeople.GroupBy(p => p.ParentName).Where(p => p.Any() && !missingParents.Contains(p.Key)).AsArray(); _result.Data.Count.Should().Be(grouped.Length); - grouped.ForEach(grouping => - { - if (missingParents.Length > 0 && missingParents.Contains(grouping.Key)) - { - return; - } + grouped.ForEach( + grouping => + { + if (missingParents.Length > 0 && missingParents.Contains(grouping.Key)) + { + return; + } - var result = _result.Data.Lookup(grouping.Key) - .ValueOrThrow(() => new Exception("Missing result for " + grouping.Key)); + var result = _result.Data.Lookup(grouping.Key).ValueOrThrow(() => new Exception("Missing result for " + grouping.Key)); - var children = result.Children; - children.Should().BeEquivalentTo(grouping); - }); + var children = result.Children; + children.Should().BeEquivalentTo(grouping); + }); } private int CalculateParent(int index, int totalPeople) diff --git a/src/DynamicData.Tests/Cache/KeyValueCollectionEx.cs b/src/DynamicData.Tests/Cache/KeyValueCollectionEx.cs index aed360f99..d13a184af 100644 --- a/src/DynamicData.Tests/Cache/KeyValueCollectionEx.cs +++ b/src/DynamicData.Tests/Cache/KeyValueCollectionEx.cs @@ -5,12 +5,10 @@ namespace DynamicData.Tests.Cache { public static class KeyValueCollectionEx { - public static IDictionary> Indexed(this - IKeyValueCollection source) + public static IDictionary> Indexed(this IKeyValueCollection source) + where TKey : notnull { - return source - .Select((kv, idx) => new IndexedItem(kv.Value, kv.Key, idx)) - .ToDictionary(i => i.Key); + return source.Select((kv, idx) => new IndexedItem(kv.Value, kv.Key, idx)).ToDictionary(i => i.Key); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/LeftJoinFixture.cs b/src/DynamicData.Tests/Cache/LeftJoinFixture.cs index 4cf8eaea0..57e07eaee 100644 --- a/src/DynamicData.Tests/Cache/LeftJoinFixture.cs +++ b/src/DynamicData.Tests/Cache/LeftJoinFixture.cs @@ -1,43 +1,40 @@ using System; using System.Linq; + using DynamicData.Kernel; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - public class LeftJoinFixture: IDisposable + public class LeftJoinFixture : IDisposable { private readonly SourceCache _left; - private readonly SourceCache _right; + private readonly ChangeSetAggregator _result; - public LeftJoinFixture() + private readonly SourceCache _right; + + public LeftJoinFixture() { _left = new SourceCache(device => device.Name); _right = new SourceCache(device => device.Name); - _result = _left.Connect() - .LeftJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(device, meta)) - .AsAggregator(); - } - - public void Dispose() - { - _left.Dispose(); - _right.Dispose(); - _result.Dispose(); + _result = _left.Connect().LeftJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(device, meta)).AsAggregator(); } [Fact] public void AddLeftOnly() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Lookup("Device1").HasValue.Should().BeTrue(); @@ -47,57 +44,93 @@ public void AddLeftOnly() _result.Data.Items.All(dwm => dwm.MetaData == Optional.None).Should().BeTrue(); } + [Fact] + public void AddLetThenRight() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + 3.Should().Be(_result.Data.Count); + + _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); + } + [Fact] public void AddRightOnly() { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); 0.Should().Be(_result.Data.Count); } [Fact] - public void AddLetThenRight() + public void AddRightThenLeft() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); } + public void Dispose() + { + _left.Dispose(); + _right.Dispose(); + _result.Dispose(); + } + [Fact] public void RemoveVarious() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); _right.Remove("Device3"); @@ -108,44 +141,24 @@ public void RemoveVarious() _result.Data.Lookup("Device1").HasValue.Should().BeFalse(); } - [Fact] - public void AddRightThenLeft() - { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); - - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - 3.Should().Be(_result.Data.Count); - - _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); - } - [Fact] public void UpdateRight() { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); - - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); @@ -154,16 +167,24 @@ public void UpdateRight() public class Device : IEquatable { - public string Name { get; } - public Device(string name) { Name = name; } - #region Equality Members + public string Name { get; } - public bool Equals(Device other) + public static bool operator ==(Device left, Device right) + { + return Equals(left, right); + } + + public static bool operator !=(Device left, Device right) + { + return !Equals(left, right); + } + + public bool Equals(Device? other) { if (ReferenceEquals(null, other)) { @@ -178,7 +199,7 @@ public bool Equals(Device other) return string.Equals(Name, other.Name); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -200,21 +221,9 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Name != null ? Name.GetHashCode() : 0); + return (Name is not null ? Name.GetHashCode() : 0); } - public static bool operator ==(Device left, Device right) - { - return Equals(left, right); - } - - public static bool operator !=(Device left, Device right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{Name}"; @@ -223,19 +232,27 @@ public override string ToString() public class DeviceMetaData : IEquatable { - public string Name { get; } - - public bool IsAutoConnect { get; } - public DeviceMetaData(string name, bool isAutoConnect = false) { Name = name; IsAutoConnect = isAutoConnect; } - #region Equality members + public bool IsAutoConnect { get; } + + public string Name { get; } - public bool Equals(DeviceMetaData other) + public static bool operator ==(DeviceMetaData left, DeviceMetaData right) + { + return Equals(left, right); + } + + public static bool operator !=(DeviceMetaData left, DeviceMetaData right) + { + return !Equals(left, right); + } + + public bool Equals(DeviceMetaData? other) { if (ReferenceEquals(null, other)) { @@ -250,7 +267,7 @@ public bool Equals(DeviceMetaData other) return string.Equals(Name, other.Name) && IsAutoConnect == other.IsAutoConnect; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -274,22 +291,10 @@ public override int GetHashCode() { unchecked { - return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ IsAutoConnect.GetHashCode(); + return (Name.GetHashCode() * 397) ^ IsAutoConnect.GetHashCode(); } } - public static bool operator ==(DeviceMetaData left, DeviceMetaData right) - { - return Equals(left, right); - } - - public static bool operator !=(DeviceMetaData left, DeviceMetaData right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"Metadata: {Name}. IsAutoConnect = {IsAutoConnect}"; @@ -298,18 +303,27 @@ public override string ToString() public class DeviceWithMetadata : IEquatable { - public Device Device { get; } - public Optional MetaData { get; } - public DeviceWithMetadata(Device device, Optional metaData) { Device = device; MetaData = metaData; } - #region Equality members + public Device Device { get; } + + public Optional MetaData { get; } + + public static bool operator ==(DeviceWithMetadata left, DeviceWithMetadata right) + { + return Equals(left, right); + } - public bool Equals(DeviceWithMetadata other) + public static bool operator !=(DeviceWithMetadata left, DeviceWithMetadata right) + { + return !Equals(left, right); + } + + public bool Equals(DeviceWithMetadata? other) { if (ReferenceEquals(null, other)) { @@ -324,7 +338,7 @@ public bool Equals(DeviceWithMetadata other) return Equals(Device, other.Device) && MetaData.Equals(other.MetaData); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -336,38 +350,21 @@ public override bool Equals(object obj) return true; } - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((DeviceWithMetadata)obj); + return obj is DeviceWithMetadata value && Equals(value); } public override int GetHashCode() { unchecked { - return ((Device != null ? Device.GetHashCode() : 0) * 397) ^ MetaData.GetHashCode(); + return (Device.GetHashCode() * 397) ^ MetaData.GetHashCode(); } } - public static bool operator ==(DeviceWithMetadata left, DeviceWithMetadata right) - { - return Equals(left, right); - } - - public static bool operator !=(DeviceWithMetadata left, DeviceWithMetadata right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{Device} ({MetaData})"; } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/LeftJoinManyFixture.cs b/src/DynamicData.Tests/Cache/LeftJoinManyFixture.cs index cd3db02e5..55f3c2686 100644 --- a/src/DynamicData.Tests/Cache/LeftJoinManyFixture.cs +++ b/src/DynamicData.Tests/Cache/LeftJoinManyFixture.cs @@ -1,38 +1,52 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class LeftJoinManyFixture: IDisposable + public class LeftJoinManyFixture : IDisposable { private readonly SourceCache _people; + private readonly ChangeSetAggregator _result; - public LeftJoinManyFixture() + public LeftJoinManyFixture() { _people = new SourceCache(p => p.Name); - _result = _people.Connect() - .LeftJoinMany(_people.Connect(), pac => pac.ParentName, (person, grouping) => new ParentAndChildren(person, grouping.Items.Select(p => p).ToArray())) - .AsAggregator(); + _result = _people.Connect().LeftJoinMany(_people.Connect(), pac => pac.ParentName, (person, grouping) => new ParentAndChildren(person, grouping.Items.Select(p => p).ToArray())).AsAggregator(); } - public void Dispose() + [Fact] + public void AddChild() { - _people.Dispose(); - _result.Dispose(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); + + _people.AddOrUpdate(people); + + var person11 = new Person("Person11", 100, parentName: "Person3"); + _people.AddOrUpdate(person11); + + var updatedPeople = people.Union(new[] { person11 }).ToArray(); + + AssertDataIsCorrectlyFormed(updatedPeople); } [Fact] public void AddLeftOnly() { - var people = Enumerable.Range(1, 10) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + var people = Enumerable.Range(1, 10).Select(i => new Person("Person" + i, i)).ToArray(); _people.AddOrUpdate(people); @@ -45,51 +59,53 @@ public void AddLeftOnly() [Fact] public void AddPeopleWithParents() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); AssertDataIsCorrectlyFormed(people); } + public void Dispose() + { + _people.Dispose(); + _result.Dispose(); + } + [Fact] - public void UpdateParent() + public void RemoveChild() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); - var current10 = people.Last(); - var person10 = new Person("Person10", 100, parentName: current10.ParentName); - _people.AddOrUpdate(person10); + var last = people.Last(); + _people.Remove(last); - var updatedPeople = people.Take(9).Union(new[] {person10}).ToArray(); + var updatedPeople = people.Where(p => p.Name != last.Name).ToArray(); - AssertDataIsCorrectlyFormed(updatedPeople); + AssertDataIsCorrectlyFormed(updatedPeople, last.Name); } [Fact] public void UpdateChild() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); @@ -97,72 +113,50 @@ public void UpdateChild() var person6 = new Person("Person6", 100, parentName: current6.ParentName); _people.AddOrUpdate(person6); - var updatedPeople = people.Where(p => p.Name != "Person6").Union(new[] {person6}).ToArray(); + var updatedPeople = people.Where(p => p.Name != "Person6").Union(new[] { person6 }).ToArray(); AssertDataIsCorrectlyFormed(updatedPeople); } [Fact] - public void AddChild() + public void UpdateParent() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); - var person11 = new Person("Person11", 100, parentName: "Person3"); - _people.AddOrUpdate(person11); + var current10 = people.Last(); + var person10 = new Person("Person10", 100, parentName: current10.ParentName); + _people.AddOrUpdate(person10); - var updatedPeople = people.Union(new[] {person11}).ToArray(); + var updatedPeople = people.Take(9).Union(new[] { person10 }).ToArray(); AssertDataIsCorrectlyFormed(updatedPeople); } - [Fact] - public void RemoveChild() - { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); - - _people.AddOrUpdate(people); - - var last = people.Last(); - _people.Remove(last); - - var updatedPeople = people.Where(p => p.Name != last.Name).ToArray(); - - AssertDataIsCorrectlyFormed(updatedPeople, last.Name); - } - private void AssertDataIsCorrectlyFormed(Person[] expected, params string[] missingParents) { _result.Data.Count.Should().Be(expected.Length); _result.Data.Items.Select(pac => pac.Parent).Should().BeEquivalentTo(expected); - expected.GroupBy(p => p.ParentName) - .ForEach(grouping => - { - if (missingParents.Length > 0 && missingParents.Contains(grouping.Key)) + expected.GroupBy(p => p.ParentName).ForEach( + grouping => { - return; - } + if (missingParents.Length > 0 && missingParents.Contains(grouping.Key)) + { + return; + } - var result = _result.Data.Lookup(grouping.Key) - .ValueOrThrow(() => new Exception("Missing result for " + grouping.Key)); + var result = _result.Data.Lookup(grouping.Key).ValueOrThrow(() => new Exception("Missing result for " + grouping.Key)); - var children = result.Children; - children.Should().BeEquivalentTo(grouping); - }); + var children = result.Children; + children.Should().BeEquivalentTo(grouping); + }); } private int CalculateParent(int index, int totalPeople) diff --git a/src/DynamicData.Tests/Cache/MergeManyFixture.cs b/src/DynamicData.Tests/Cache/MergeManyFixture.cs index 1b97e2022..2e447da2a 100644 --- a/src/DynamicData.Tests/Cache/MergeManyFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyFixture.cs @@ -1,38 +1,18 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - - public class MergeManyFixture: IDisposable + public class MergeManyFixture : IDisposable { - private class ObjectWithObservable - { - private readonly ISubject _changed = new Subject(); - private bool _value; - - public ObjectWithObservable(int id) - { - Id = id; - } - - public void InvokeObservable(bool value) - { - _value = value; - _changed.OnNext(value); - } - - public IObservable Observable => _changed.AsObservable(); - - public int Id { get; } - } - private readonly SourceCache _source; - public MergeManyFixture() + public MergeManyFixture() { _source = new SourceCache(p => p.Id); } @@ -42,6 +22,21 @@ public void Dispose() _source.Dispose(); } + [Fact] + public void EverythingIsUnsubscribedWhenStreamIsDisposed() + { + bool invoked = false; + var stream = _source.Connect().MergeMany(o => o.Observable).Subscribe(o => { invoked = true; }); + + var item = new ObjectWithObservable(1); + _source.AddOrUpdate(item); + + stream.Dispose(); + + item.InvokeObservable(true); + invoked.Should().BeFalse(); + } + /// /// Invocations the only when child is invoked. /// @@ -50,9 +45,7 @@ public void InvocationOnlyWhenChildIsInvoked() { bool invoked = false; - var stream = _source.Connect() - .MergeMany(o => o.Observable) - .Subscribe(o => { invoked = true; }); + var stream = _source.Connect().MergeMany(o => o.Observable).Subscribe(o => { invoked = true; }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); @@ -68,9 +61,7 @@ public void InvocationOnlyWhenChildIsInvoked() public void RemovedItemWillNotCauseInvocation() { bool invoked = false; - var stream = _source.Connect() - .MergeMany(o => o.Observable) - .Subscribe(o => { invoked = true; }); + var stream = _source.Connect().MergeMany(o => o.Observable).Subscribe(o => { invoked = true; }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); @@ -82,21 +73,26 @@ public void RemovedItemWillNotCauseInvocation() stream.Dispose(); } - [Fact] - public void EverythingIsUnsubscribedWhenStreamIsDisposed() + private class ObjectWithObservable { - bool invoked = false; - var stream = _source.Connect() - .MergeMany(o => o.Observable) - .Subscribe(o => { invoked = true; }); + private readonly ISubject _changed = new Subject(); - var item = new ObjectWithObservable(1); - _source.AddOrUpdate(item); + private bool _value; - stream.Dispose(); + public ObjectWithObservable(int id) + { + Id = id; + } - item.InvokeObservable(true); - invoked.Should().BeFalse(); + public int Id { get; } + + public IObservable Observable => _changed.AsObservable(); + + public void InvokeObservable(bool value) + { + _value = value; + _changed.OnNext(value); + } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/MergeManyItemsFixture.cs b/src/DynamicData.Tests/Cache/MergeManyItemsFixture.cs index 68b9c448a..59159a9d1 100644 --- a/src/DynamicData.Tests/Cache/MergeManyItemsFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyItemsFixture.cs @@ -1,39 +1,18 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - - public class MergeManyItemsFixture: IDisposable + public class MergeManyItemsFixture : IDisposable { - private class ObjectWithObservable - { - private readonly int _id; - private readonly ISubject _changed = new Subject(); - private bool _value; - - public ObjectWithObservable(int id) - { - _id = id; - } - - public void InvokeObservable(bool value) - { - _value = value; - _changed.OnNext(value); - } - - public IObservable Observable => _changed.AsObservable(); - - public int Id => _id; - } - private readonly ISourceCache _source; - public MergeManyItemsFixture() + public MergeManyItemsFixture() { _source = new SourceCache(p => p.Id); } @@ -43,18 +22,37 @@ public void Dispose() _source.Dispose(); } + [Fact] + public void EverythingIsUnsubscribedWhenStreamIsDisposed() + { + bool invoked = false; + var stream = _source.Connect().MergeManyItems(o => o.Observable).Subscribe( + o => + { + invoked = true; + (o.Item.Id == 1).Should().BeTrue(); + }); + + var item = new ObjectWithObservable(1); + _source.AddOrUpdate(item); + + stream.Dispose(); + + item.InvokeObservable(true); + invoked.Should().BeFalse(); + } + [Fact] public void InvocationOnlyWhenChildIsInvoked() { bool invoked = false; - var stream = _source.Connect() - .MergeManyItems(o => o.Observable) - .Subscribe(o => - { - invoked = true; - (o.Item.Id == 1).Should().BeTrue(); - }); + var stream = _source.Connect().MergeManyItems(o => o.Observable).Subscribe( + o => + { + invoked = true; + (o.Item.Id == 1).Should().BeTrue(); + }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); @@ -70,13 +68,12 @@ public void InvocationOnlyWhenChildIsInvoked() public void RemovedItemWillNotCauseInvocation() { bool invoked = false; - var stream = _source.Connect() - .MergeManyItems(o => o.Observable) - .Subscribe(o => - { - invoked = true; - (o.Item.Id == 1).Should().BeTrue(); - }); + var stream = _source.Connect().MergeManyItems(o => o.Observable).Subscribe( + o => + { + invoked = true; + (o.Item.Id == 1).Should().BeTrue(); + }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); @@ -88,25 +85,26 @@ public void RemovedItemWillNotCauseInvocation() stream.Dispose(); } - [Fact] - public void EverythingIsUnsubscribedWhenStreamIsDisposed() + private class ObjectWithObservable { - bool invoked = false; - var stream = _source.Connect() - .MergeManyItems(o => o.Observable) - .Subscribe(o => - { - invoked = true; - (o.Item.Id == 1).Should().BeTrue(); - }); + private readonly ISubject _changed = new Subject(); - var item = new ObjectWithObservable(1); - _source.AddOrUpdate(item); + private bool _value; - stream.Dispose(); + public ObjectWithObservable(int id) + { + Id = id; + } - item.InvokeObservable(true); - invoked.Should().BeFalse(); + public int Id { get; } + + public IObservable Observable => _changed.AsObservable(); + + public void InvokeObservable(bool value) + { + _value = value; + _changed.OnNext(value); + } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/MergeManyWithKeyOverloadFixture.cs b/src/DynamicData.Tests/Cache/MergeManyWithKeyOverloadFixture.cs index 0798499ac..6ae78d4f0 100644 --- a/src/DynamicData.Tests/Cache/MergeManyWithKeyOverloadFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyWithKeyOverloadFixture.cs @@ -1,48 +1,18 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - - public class MergeManyWithKeyOverloadFixture: IDisposable + public class MergeManyWithKeyOverloadFixture : IDisposable { - private class ObjectWithObservable - { - private readonly ISubject _changed = new Subject(); - private bool _value; - - public ObjectWithObservable(int id) - { - Id = id; - } - - public void InvokeObservable(bool value) - { - _value = value; - _changed.OnNext(value); - } - - public void CompleteObservable() - { - _changed.OnCompleted(); - } - - public void FailObservable(Exception ex) - { - _changed.OnError(ex); - } - - public IObservable Observable => _changed.AsObservable(); - - public int Id { get; } - } - private readonly ISourceCache _source; - public MergeManyWithKeyOverloadFixture() + public MergeManyWithKeyOverloadFixture() { _source = new SourceCache(p => p.Id); } @@ -53,70 +23,58 @@ public void Dispose() } [Fact] - public void InvocationOnlyWhenChildIsInvoked() + public void EverythingIsUnsubscribedWhenStreamIsDisposed() { - var invoked = false; - - var stream = _source.Connect() - .MergeMany((o, key) => o.Observable) - .Subscribe(o => { invoked = true; }); + bool invoked = false; + var stream = _source.Connect().MergeMany((o, key) => o.Observable).Subscribe(o => { invoked = true; }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); - invoked.Should().BeFalse(); + stream.Dispose(); item.InvokeObservable(true); - invoked.Should().BeTrue(); - stream.Dispose(); + invoked.Should().BeFalse(); } [Fact] - public void RemovedItemWillNotCauseInvocation() + public void InvocationOnlyWhenChildIsInvoked() { - bool invoked = false; - var stream = _source.Connect() - .MergeMany((o, key) => o.Observable) - .Subscribe(o => { invoked = true; }); + var invoked = false; + + var stream = _source.Connect().MergeMany((o, key) => o.Observable).Subscribe(o => { invoked = true; }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); - _source.Remove(item); + invoked.Should().BeFalse(); item.InvokeObservable(true); - invoked.Should().BeFalse(); + invoked.Should().BeTrue(); stream.Dispose(); } [Fact] - public void EverythingIsUnsubscribedWhenStreamIsDisposed() + public void RemovedItemWillNotCauseInvocation() { bool invoked = false; - var stream = _source.Connect() - .MergeMany((o, key) => o.Observable) - .Subscribe(o => { invoked = true; }); + var stream = _source.Connect().MergeMany((o, key) => o.Observable).Subscribe(o => { invoked = true; }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); - - stream.Dispose(); + _source.Remove(item); + invoked.Should().BeFalse(); item.InvokeObservable(true); invoked.Should().BeFalse(); + stream.Dispose(); } [Fact] public void SingleItemCompleteWillNotMergedStream() { var completed = false; - var stream = - _source.Connect() - .MergeMany((o, key) => o.Observable) - .Subscribe( - _ => {}, - () => completed = true - ); + var stream = _source.Connect().MergeMany((o, key) => o.Observable).Subscribe(_ => { }, () => completed = true); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); @@ -133,13 +91,7 @@ public void SingleItemCompleteWillNotMergedStream() public void SingleItemFailWillNotFailMergedStream() { var failed = false; - var stream = - _source.Connect() - .MergeMany((o, key) => o.Observable) - .Subscribe( - _ => { }, - ex => failed = true - ); + var stream = _source.Connect().MergeMany((o, key) => o.Observable).Subscribe(_ => { }, ex => failed = true); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); @@ -151,5 +103,36 @@ public void SingleItemFailWillNotFailMergedStream() failed.Should().BeFalse(); } + private class ObjectWithObservable + { + private readonly ISubject _changed = new Subject(); + + private bool _value; + + public ObjectWithObservable(int id) + { + Id = id; + } + + public int Id { get; } + + public IObservable Observable => _changed.AsObservable(); + + public void CompleteObservable() + { + _changed.OnCompleted(); + } + + public void FailObservable(Exception ex) + { + _changed.OnError(ex); + } + + public void InvokeObservable(bool value) + { + _value = value; + _changed.OnNext(value); + } + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/MonitorStatusFixture.cs b/src/DynamicData.Tests/Cache/MonitorStatusFixture.cs index 95cfca7f5..772ca0232 100644 --- a/src/DynamicData.Tests/Cache/MonitorStatusFixture.cs +++ b/src/DynamicData.Tests/Cache/MonitorStatusFixture.cs @@ -1,13 +1,15 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Kernel; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class MonitorStatusFixture { [Fact] @@ -15,32 +17,36 @@ public void InitialiStatusIsLoadding() { bool invoked = false; var status = ConnectionStatus.Pending; - var subscription = new Subject().MonitorStatus().Subscribe(s => - { - invoked = true; - status = s; - }); + var subscription = new Subject().MonitorStatus().Subscribe( + s => + { + invoked = true; + status = s; + }); invoked.Should().BeTrue(); status.Should().Be(ConnectionStatus.Pending, "No status has been received"); subscription.Dispose(); } [Fact] - public void SetToLoaded() + public void MultipleInvokesDoNotCallLoadedAgain() { bool invoked = false; - var status = ConnectionStatus.Pending; + int invocations = 0; var subject = new Subject(); - var subscription = subject.MonitorStatus() - .Subscribe(s => - { - invoked = true; - status = s; - }); + var subscription = subject.MonitorStatus().Where(status => status == ConnectionStatus.Loaded).Subscribe( + s => + { + invoked = true; + invocations++; + }); subject.OnNext(1); + subject.OnNext(1); + subject.OnNext(1); + invoked.Should().BeTrue(); - status.Should().Be(ConnectionStatus.Loaded, "Status should be ConnectionStatus.Loaded"); + invocations.Should().Be(1, "Status should be ConnectionStatus.Loaded"); subscription.Dispose(); } @@ -52,12 +58,13 @@ public void SetToError() var subject = new Subject(); Exception exception; - var subscription = subject.MonitorStatus() - .Subscribe(s => - { - invoked = true; - status = s; - }, ex => { exception = ex; }); + var subscription = subject.MonitorStatus().Subscribe( + s => + { + invoked = true; + status = s; + }, + ex => { exception = ex; }); subject.OnError(new Exception("Test")); subscription.Dispose(); @@ -67,26 +74,22 @@ public void SetToError() } [Fact] - public void MultipleInvokesDoNotCallLoadedAgain() + public void SetToLoaded() { bool invoked = false; - int invocations = 0; + var status = ConnectionStatus.Pending; var subject = new Subject(); - var subscription = subject.MonitorStatus() - .Where(status => status == ConnectionStatus.Loaded) - .Subscribe(s => - { - invoked = true; - invocations++; - }); + var subscription = subject.MonitorStatus().Subscribe( + s => + { + invoked = true; + status = s; + }); subject.OnNext(1); - subject.OnNext(1); - subject.OnNext(1); - invoked.Should().BeTrue(); - invocations.Should().Be(1, "Status should be ConnectionStatus.Loaded"); + status.Should().Be(ConnectionStatus.Loaded, "Status should be ConnectionStatus.Loaded"); subscription.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ObservableCachePreviewFixture.cs b/src/DynamicData.Tests/Cache/ObservableCachePreviewFixture.cs index e636c01a0..040e019a2 100644 --- a/src/DynamicData.Tests/Cache/ObservableCachePreviewFixture.cs +++ b/src/DynamicData.Tests/Cache/ObservableCachePreviewFixture.cs @@ -1,76 +1,83 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using Xunit; namespace DynamicData.Tests.Cache { public class ObservableCachePreviewFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; + private readonly ISourceCache _source; + public ObservableCachePreviewFixture() { _source = new SourceCache(p => p.Name); _results = _source.Connect().AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] - public void NoChangesAllowedDuringPreview() + public void ChangesAreNotYetAppliedDuringPreview() { - // On preview, try adding an arbitrary item - var d = _source.Preview().Subscribe(_ => - { - Assert.Throws(() => _source.AddOrUpdate(new Person("A", 1))); - }); + _source.Clear(); + + // On preview, make sure the list is empty + var d = _source.Preview().Subscribe( + _ => + { + Assert.True(_source.Count == 0); + Assert.True(_source.Items.Count() == 0); + }); // Trigger a change - _source.AddOrUpdate(new Person("B", 2)); + _source.AddOrUpdate(new Person("A", 1)); // Cleanup d.Dispose(); } [Fact] - public void RecursiveEditsWork() + public void ConnectPreviewPredicateIsApplied() { - var person = new Person("A", 1); + _source.Clear(); - _source.Edit(l => - { - _source.AddOrUpdate(person); - Assert.True(_source.Items.SequenceEqual(new[] { person })); - Assert.True(l.Items.SequenceEqual(new[] { person })); - }); + // Collect preview messages about even numbers only + var aggregator = _source.Preview(i => i.Age == 2).AsAggregator(); - Assert.True(_source.Items.SequenceEqual(new[] { person })); + // Trigger changes + _source.AddOrUpdate(new Person("A", 1)); + _source.AddOrUpdate(new Person("B", 2)); + _source.AddOrUpdate(new Person("C", 3)); + + Assert.True(aggregator.Messages.Count == 1); + Assert.True(aggregator.Messages[0].Count == 1); + Assert.True(aggregator.Messages[0].First().Key == "B"); + Assert.True(aggregator.Messages[0].First().Reason == ChangeReason.Add); + + // Cleanup + aggregator.Dispose(); } - [Fact] - public void RecursiveEditsHavePostponedEvents() + public void Dispose() { - var person = new Person("A", 1); + _source.Dispose(); + _results.Dispose(); + } - var preview = _source.Preview().AsAggregator(); - var connect = _source.Connect().AsAggregator(); - _source.Edit(l => - { - _source.Edit(l2 => l2.AddOrUpdate(person)); - Assert.Equal(0, preview.Messages.Count); - Assert.Equal(0, connect.Messages.Count); - }); + [Fact] + public void NoChangesAllowedDuringPreview() + { + // On preview, try adding an arbitrary item + var d = _source.Preview().Subscribe(_ => { Assert.Throws(() => _source.AddOrUpdate(new Person("A", 1))); }); - Assert.Equal(1, preview.Messages.Count); - Assert.Equal(1, connect.Messages.Count); + // Trigger a change + _source.AddOrUpdate(new Person("B", 2)); - Assert.True(_source.Items.SequenceEqual(new[] { person })); + // Cleanup + d.Dispose(); } [Fact] @@ -80,56 +87,53 @@ public void PreviewEventsAreCorrect() var preview = _source.Preview().AsAggregator(); var connect = _source.Connect().AsAggregator(); - _source.Edit(l => - { - _source.Edit(l2 => l2.AddOrUpdate(person)); - l.Remove(person); - l.AddOrUpdate(new[] { new Person("B", 2), new Person("C", 3) }); - }); + _source.Edit( + l => + { + _source.Edit(l2 => l2.AddOrUpdate(person)); + l.Remove(person); + l.AddOrUpdate(new[] { new Person("B", 2), new Person("C", 3) }); + }); Assert.True(preview.Messages.SequenceEqual(connect.Messages)); Assert.True(_source.KeyValues.OrderBy(t => t.Value.Age).Select(t => t.Value.Age).SequenceEqual(new[] { 2, 3 })); } [Fact] - public void ChangesAreNotYetAppliedDuringPreview() + public void RecursiveEditsHavePostponedEvents() { - _source.Clear(); + var person = new Person("A", 1); - // On preview, make sure the list is empty - var d = _source.Preview().Subscribe(_ => - { - Assert.True(_source.Count == 0); - Assert.True(_source.Items.Count() == 0); - }); + var preview = _source.Preview().AsAggregator(); + var connect = _source.Connect().AsAggregator(); + _source.Edit( + l => + { + _source.Edit(l2 => l2.AddOrUpdate(person)); + Assert.Equal(0, preview.Messages.Count); + Assert.Equal(0, connect.Messages.Count); + }); - // Trigger a change - _source.AddOrUpdate(new Person("A", 1)); + Assert.Equal(1, preview.Messages.Count); + Assert.Equal(1, connect.Messages.Count); - // Cleanup - d.Dispose(); + Assert.True(_source.Items.SequenceEqual(new[] { person })); } [Fact] - public void ConnectPreviewPredicateIsApplied() + public void RecursiveEditsWork() { - _source.Clear(); - - // Collect preview messages about even numbers only - var aggregator = _source.Preview(i => i.Age == 2).AsAggregator(); - - // Trigger changes - _source.AddOrUpdate(new Person("A", 1)); - _source.AddOrUpdate(new Person("B", 2)); - _source.AddOrUpdate(new Person("C", 3)); + var person = new Person("A", 1); - Assert.True(aggregator.Messages.Count == 1); - Assert.True(aggregator.Messages[0].Count == 1); - Assert.True(aggregator.Messages[0].First().Key == "B"); - Assert.True(aggregator.Messages[0].First().Reason == ChangeReason.Add); + _source.Edit( + l => + { + _source.AddOrUpdate(person); + Assert.True(_source.Items.SequenceEqual(new[] { person })); + Assert.True(l.Items.SequenceEqual(new[] { person })); + }); - // Cleanup - aggregator.Dispose(); + Assert.True(_source.Items.SequenceEqual(new[] { person })); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ObservableChangeSetFixture.cs b/src/DynamicData.Tests/Cache/ObservableChangeSetFixture.cs index 60dcc8787..703a172a9 100644 --- a/src/DynamicData.Tests/Cache/ObservableChangeSetFixture.cs +++ b/src/DynamicData.Tests/Cache/ObservableChangeSetFixture.cs @@ -4,29 +4,67 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class ObservableChangeSetFixture { + [Fact] + public void HandlesAsyncError() + { + Exception? error = null; + + Task> Loader() + { + throw new Exception("Broken"); + } + + var observable = ObservableChangeSet.Create( + async cache => + { + var people = await Loader(); + cache.AddOrUpdate(people); + return () => { }; + }, + p => p.Name); + + using (var dervived = observable.AsObservableCache()) + using (dervived.Connect().Subscribe(_ => { }, ex => error = ex)) + { + error.Should().NotBeNull(); + } + } [Fact] - public void LoadsAndDisposeUsingAction() + public void HandlesError() { - bool isDisposed = false; - SubscribeAndAssert(ObservableChangeSet.Create(cache => + Exception? error = null; + + IEnumerable Loader() { - Person[] people = Enumerable.Range(1, 100).Select(i => new Person($"Name.{i}", i)).ToArray(); - cache.AddOrUpdate(people); - return () => { isDisposed = true; }; - }, p => p.Name), - checkContentAction: result => result.Count.Should().Be(100)); + throw new Exception("Broken"); + } - isDisposed.Should().BeTrue(); + var observable = ObservableChangeSet.Create( + cache => + { + var people = Loader(); + cache.AddOrUpdate(people); + return () => { }; + }, + p => p.Name); + + using (var dervived = observable.AsObservableCache()) + using (dervived.Connect().Subscribe(_ => { }, ex => error = ex)) + { + error.Should().NotBeNull(); + } } [Fact] @@ -34,26 +72,26 @@ public void LoadsAndDisposeFromObservableCache() { bool isDisposed = false; - var observable = ObservableChangeSet.Create(cache => - { - return () => { isDisposed = true; }; - }, p => p.Name); + var observable = ObservableChangeSet.Create(cache => { return () => { isDisposed = true; }; }, p => p.Name); observable.AsObservableCache().Dispose(); isDisposed.Should().BeTrue(); } [Fact] - public void LoadsAndDisposeUsingDisposable() + public void LoadsAndDisposeUsingAction() { bool isDisposed = false; - SubscribeAndAssert(ObservableChangeSet.Create(cache => - { - Person[] people = Enumerable.Range(1, 100).Select(i => new Person($"Name.{i}", i)).ToArray(); - cache.AddOrUpdate(people); - return Disposable.Create(()=> { isDisposed = true; }); - }, p => p.Name), - checkContentAction: result => result.Count.Should().Be(100)); + SubscribeAndAssert( + ObservableChangeSet.Create( + cache => + { + Person[] people = Enumerable.Range(1, 100).Select(i => new Person($"Name.{i}", i)).ToArray(); + cache.AddOrUpdate(people); + return () => { isDisposed = true; }; + }, + p => p.Name), + checkContentAction: result => result.Count.Should().Be(100)); isDisposed.Should().BeTrue(); } @@ -64,89 +102,66 @@ public void LoadsAndDisposeUsingActionAsync() Task CreateTask() => Task.FromResult(Enumerable.Range(1, 100).Select(i => new Person($"Name.{i}", i)).ToArray()); bool isDisposed = false; - SubscribeAndAssert(ObservableChangeSet.Create(async cache => - { - var people = await CreateTask(); - cache.AddOrUpdate(people); - return () => { isDisposed = true; }; - }, p => p.Name), - checkContentAction: result => result.Count.Should().Be(100)); + SubscribeAndAssert( + ObservableChangeSet.Create( + async cache => + { + var people = await CreateTask(); + cache.AddOrUpdate(people); + return () => { isDisposed = true; }; + }, + p => p.Name), + checkContentAction: result => result.Count.Should().Be(100)); isDisposed.Should().BeTrue(); } [Fact] - public void LoadsAndDisposeUsingDisposableAsync() + public void LoadsAndDisposeUsingDisposable() { - Task CreateTask() => Task.FromResult(Enumerable.Range(1, 100).Select(i => new Person($"Name.{i}", i)).ToArray()); - bool isDisposed = false; - SubscribeAndAssert(ObservableChangeSet.Create(async cache => - { - var people = await CreateTask(); - cache.AddOrUpdate(people); - return Disposable.Create(() => { isDisposed = true; }); - }, p => p.Name), - checkContentAction: result => result.Count.Should().Be(100)); + SubscribeAndAssert( + ObservableChangeSet.Create( + cache => + { + Person[] people = Enumerable.Range(1, 100).Select(i => new Person($"Name.{i}", i)).ToArray(); + cache.AddOrUpdate(people); + return Disposable.Create(() => { isDisposed = true; }); + }, + p => p.Name), + checkContentAction: result => result.Count.Should().Be(100)); isDisposed.Should().BeTrue(); } [Fact] - public void HandlesAsyncError() - { - Exception error = null; - Task> Loader() - { - throw new Exception("Broken"); - } - - var observable = ObservableChangeSet.Create(async cache => - { - var people = await Loader(); - cache.AddOrUpdate(people); - return () => { }; - }, p => p.Name); - - using (var dervived = observable.AsObservableCache()) - using (dervived.Connect().Subscribe(_ => { }, ex => error = ex )) - { - error.Should().NotBeNull(); - } - } - - [Fact] - public void HandlesError() + public void LoadsAndDisposeUsingDisposableAsync() { - Exception error = null; - IEnumerable Loader() - { - throw new Exception("Broken"); - } + Task CreateTask() => Task.FromResult(Enumerable.Range(1, 100).Select(i => new Person($"Name.{i}", i)).ToArray()); - var observable = ObservableChangeSet.Create(cache => - { - var people = Loader(); - cache.AddOrUpdate(people); - return () => { }; - }, p => p.Name); + bool isDisposed = false; + SubscribeAndAssert( + ObservableChangeSet.Create( + async cache => + { + var people = await CreateTask(); + cache.AddOrUpdate(people); + return Disposable.Create(() => { isDisposed = true; }); + }, + p => p.Name), + checkContentAction: result => result.Count.Should().Be(100)); - using (var dervived = observable.AsObservableCache()) - using (dervived.Connect().Subscribe(_ => { }, ex => error = ex)) - { - error.Should().NotBeNull(); - } + isDisposed.Should().BeTrue(); } - private void SubscribeAndAssert(IObservable> observableChangeset, - bool expectsError = false, - Action> checkContentAction = null) + private void SubscribeAndAssert(IObservable> observableChangeset, bool expectsError = false, Action>? checkContentAction = null) + where TKey : notnull { - Exception error = null; + Exception? error = null; bool complete = false; - IChangeSet changes = null; + IChangeSet? changes = null; - using (var cache = observableChangeset.Finally(()=> complete = true).AsObservableCache()) + using (var cache = observableChangeset.Finally(() => complete = true).AsObservableCache()) using (cache.Connect().Subscribe(result => changes = result, ex => error = ex)) { if (!expectsError) @@ -164,4 +179,4 @@ private void SubscribeAndAssert(IObservable(); - - var results = subject.ToObservableChangeSet(p => p.Key).AsAggregator(); - var person = new Person("A", 1); - subject.OnNext(person); - - results.Messages.Count.Should().Be(1, "Should be 1 updates"); - results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - results.Data.Items.First().Should().Be(person, "Should be same person"); - } - [Fact] - public void OnNextForAmendedItemFiresUpdate() + public void ExpireAfterTime() { var subject = new Subject(); + var scheduler = new TestScheduler(); + var results = subject.ToObservableChangeSet(t => TimeSpan.FromMinutes(1), scheduler).AsAggregator(); - var results = subject.ToObservableChangeSet(p => p.Key).AsAggregator(); - var person = new Person("A", 1); - subject.OnNext(person); + var items = Enumerable.Range(1, 200).Select(i => new Person("p" + i.ToString("000"), i)).ToArray(); + foreach (var person in items) + { + subject.OnNext(person); + } - var personamend = new Person("A", 2); - subject.OnNext(personamend); + scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); - results.Messages.Count.Should().Be(2, "Should be 2 message"); - results.Messages[1].Updates.Should().Be(1, "Should be 1 updates"); - results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - results.Data.Items.First().Should().Be(personamend, "Should be same person"); + results.Messages.Count.Should().Be(201, "Should be 201 messages"); + results.Messages.Sum(x => x.Adds).Should().Be(200, "Should be 200 adds"); + results.Messages.Sum(x => x.Removes).Should().Be(200, "Should be 200 removes"); + results.Data.Count.Should().Be(0, "Should be no data in the cache"); } [Fact] - public void OnNextProducesAndAddChangeForSingleItem() + public void ExpireAfterTimeDynamic() { - var subject = new Subject(); + var scheduler = new TestScheduler(); + var source = Observable.Interval(TimeSpan.FromSeconds(1), scheduler).Take(30).Select(i => (int)i).Select(i => new Person("p" + i.ToString("000"), i)); - var results = subject.ToObservableChangeSet(p => p.Key).AsAggregator(); - var person = new Person("A", 1); - subject.OnNext(person); + var results = source.ToObservableChangeSet(t => TimeSpan.FromSeconds(10), scheduler).AsAggregator(); - results.Messages.Count.Should().Be(1, "Should be 1 updates"); - results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - results.Data.Items.First().Should().Be(person, "Should be same person"); + scheduler.AdvanceBy(TimeSpan.FromSeconds(30).Ticks); + + results.Messages.Count.Should().Be(50, "Should be 50 messages"); + results.Messages.Sum(x => x.Adds).Should().Be(30, "Should be 30 adds"); + results.Messages.Sum(x => x.Removes).Should().Be(20, "Should be 20 removes"); + results.Data.Count.Should().Be(10, "Should be 10 items in the cache"); } [Fact] - public void LimitSizeTo() + public void ExpireAfterTimeDynamicWithKey() { - var subject = new Subject(); var scheduler = new TestScheduler(); - var results = subject.ToObservableChangeSet(p => p.Key, limitSizeTo: 100, scheduler: scheduler).AsAggregator(); - - var items = Enumerable.Range(1, 200).Select(i => new Person("p" + i.ToString("000"), i)).ToArray(); - - items.ForEach(subject.OnNext); + var source = Observable.Interval(TimeSpan.FromSeconds(1), scheduler).Take(30).Select(i => (int)i).Select(i => new Person("p" + i.ToString("000"), i)); - scheduler.Start(); + var results = source.ToObservableChangeSet(p => p.Key, t => TimeSpan.FromSeconds(10), scheduler: scheduler).AsAggregator(); - results.Messages.Sum(x => x.Adds).Should().Be(200, "Should be 200 adds"); - results.Messages.Sum(x => x.Removes).Should().Be(100, "Should be 100 removes"); - results.Data.Count.Should().Be(100); + scheduler.AdvanceBy(TimeSpan.FromSeconds(30).Ticks); - var expected = items.Skip(100).ToArray().OrderBy(p => p.Name).ToArray(); - var actual = results.Data.Items.OrderBy(p => p.Name).ToArray(); - expected.Should().BeEquivalentTo(actual, "Only second hundred should be in the cache"); + results.Messages.Count.Should().Be(50, "Should be 50 messages"); + results.Messages.Sum(x => x.Adds).Should().Be(30, "Should be 30 adds"); + results.Messages.Sum(x => x.Removes).Should().Be(20, "Should be 20 removes"); + results.Data.Count.Should().Be(10, "Should be 10 items in the cache"); } [Fact] - public void ExpireAfterTime() + public void ExpireAfterTimeWithKey() { var subject = new Subject(); var scheduler = new TestScheduler(); - var results = subject.ToObservableChangeSet(expireAfter: t => TimeSpan.FromMinutes(1), scheduler: scheduler).AsAggregator(); + var results = subject.ToObservableChangeSet(p => p.Key, t => TimeSpan.FromMinutes(1), scheduler: scheduler).AsAggregator(); var items = Enumerable.Range(1, 200).Select(i => new Person("p" + i.ToString("000"), i)).ToArray(); foreach (var person in items) @@ -96,72 +84,78 @@ public void ExpireAfterTime() scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); - results.Messages.Count.Should().Be(201, "Should be 201 messages"); + results.Messages.Count.Should().Be(400, "Should be 400 messages"); results.Messages.Sum(x => x.Adds).Should().Be(200, "Should be 200 adds"); results.Messages.Sum(x => x.Removes).Should().Be(200, "Should be 200 removes"); results.Data.Count.Should().Be(0, "Should be no data in the cache"); } [Fact] - public void ExpireAfterTimeWithKey() + public void LimitSizeTo() { var subject = new Subject(); var scheduler = new TestScheduler(); - var results = subject.ToObservableChangeSet(p => p.Key, expireAfter: t => TimeSpan.FromMinutes(1), scheduler: scheduler).AsAggregator(); + var results = subject.ToObservableChangeSet(p => p.Key, limitSizeTo: 100, scheduler: scheduler).AsAggregator(); var items = Enumerable.Range(1, 200).Select(i => new Person("p" + i.ToString("000"), i)).ToArray(); - foreach (var person in items) - { - subject.OnNext(person); - } - scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); + items.ForEach(subject.OnNext); + + scheduler.Start(); - results.Messages.Count.Should().Be(400, "Should be 400 messages"); results.Messages.Sum(x => x.Adds).Should().Be(200, "Should be 200 adds"); - results.Messages.Sum(x => x.Removes).Should().Be(200, "Should be 200 removes"); - results.Data.Count.Should().Be(0, "Should be no data in the cache"); + results.Messages.Sum(x => x.Removes).Should().Be(100, "Should be 100 removes"); + results.Data.Count.Should().Be(100); + + var expected = items.Skip(100).ToArray().OrderBy(p => p.Name).ToArray(); + var actual = results.Data.Items.OrderBy(p => p.Name).ToArray(); + expected.Should().BeEquivalentTo(actual, "Only second hundred should be in the cache"); } [Fact] - public void ExpireAfterTimeDynamic() + public void OnNextFiresAdd() { - var scheduler = new TestScheduler(); - var source = - Observable.Interval(TimeSpan.FromSeconds(1), scheduler: scheduler) - .Take(30) - .Select(i => (int)i) - .Select(i => new Person("p" + i.ToString("000"), i)); + var subject = new Subject(); - var results = source.ToObservableChangeSet(expireAfter: t => TimeSpan.FromSeconds(10), scheduler: scheduler).AsAggregator(); + var results = subject.ToObservableChangeSet(p => p.Key).AsAggregator(); + var person = new Person("A", 1); + subject.OnNext(person); - scheduler.AdvanceBy(TimeSpan.FromSeconds(30).Ticks); + results.Messages.Count.Should().Be(1, "Should be 1 updates"); + results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + results.Data.Items.First().Should().Be(person, "Should be same person"); + } + [Fact] + public void OnNextForAmendedItemFiresUpdate() + { + var subject = new Subject(); - results.Messages.Count.Should().Be(50, "Should be 50 messages"); - results.Messages.Sum(x => x.Adds).Should().Be(30, "Should be 30 adds"); - results.Messages.Sum(x => x.Removes).Should().Be(20, "Should be 20 removes"); - results.Data.Count.Should().Be(10, "Should be 10 items in the cache"); + var results = subject.ToObservableChangeSet(p => p.Key).AsAggregator(); + var person = new Person("A", 1); + subject.OnNext(person); + + var personamend = new Person("A", 2); + subject.OnNext(personamend); + + results.Messages.Count.Should().Be(2, "Should be 2 message"); + results.Messages[1].Updates.Should().Be(1, "Should be 1 updates"); + results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + results.Data.Items.First().Should().Be(personamend, "Should be same person"); } [Fact] - public void ExpireAfterTimeDynamicWithKey() + public void OnNextProducesAndAddChangeForSingleItem() { - var scheduler = new TestScheduler(); - var source = - Observable.Interval(TimeSpan.FromSeconds(1), scheduler: scheduler) - .Take(30) - .Select(i => (int)i) - .Select(i => new Person("p" + i.ToString("000"), i)); - - var results = source.ToObservableChangeSet(p => p.Key, expireAfter: t => TimeSpan.FromSeconds(10), scheduler: scheduler).AsAggregator(); + var subject = new Subject(); - scheduler.AdvanceBy(TimeSpan.FromSeconds(30).Ticks); + var results = subject.ToObservableChangeSet(p => p.Key).AsAggregator(); + var person = new Person("A", 1); + subject.OnNext(person); - results.Messages.Count.Should().Be(50, "Should be 50 messages"); - results.Messages.Sum(x => x.Adds).Should().Be(30, "Should be 30 adds"); - results.Messages.Sum(x => x.Removes).Should().Be(20, "Should be 20 removes"); - results.Data.Count.Should().Be(10, "Should be 10 items in the cache"); + results.Messages.Count.Should().Be(1, "Should be 1 updates"); + results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + results.Data.Items.First().Should().Be(person, "Should be same person"); } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/OnItemFixture.cs b/src/DynamicData.Tests/Cache/OnItemFixture.cs index 35b50f7f1..7593df2c7 100644 --- a/src/DynamicData.Tests/Cache/OnItemFixture.cs +++ b/src/DynamicData.Tests/Cache/OnItemFixture.cs @@ -1,5 +1,7 @@ using System; + using DynamicData.Tests.Domain; + using Xunit; namespace DynamicData.Tests.Cache @@ -12,9 +14,7 @@ public void OnItemAddCalled() var called = false; var source = new SourceCache(x => x.Age); - source.Connect() - .OnItemAdded(_ => called = true) - .Subscribe(); + source.Connect().OnItemAdded(_ => called = true).Subscribe(); var person = new Person("A", 1); @@ -28,9 +28,7 @@ public void OnItemRemovedCalled() var called = false; var source = new SourceCache(x => x.Age); - source.Connect() - .OnItemRemoved(_ => called = true) - .Subscribe(); + source.Connect().OnItemRemoved(_ => called = true).Subscribe(); var person = new Person("A", 1); source.AddOrUpdate(person); @@ -44,9 +42,7 @@ public void OnItemUpdatedCalled() var called = false; var source = new SourceCache(x => x.Age); - source.Connect() - .OnItemUpdated((x,y) => called = true) - .Subscribe(); + source.Connect().OnItemUpdated((x, y) => called = true).Subscribe(); var person = new Person("A", 1); source.AddOrUpdate(person); diff --git a/src/DynamicData.Tests/Cache/OrFixture.cs b/src/DynamicData.Tests/Cache/OrFixture.cs index d5efa2f90..8c7696b11 100644 --- a/src/DynamicData.Tests/Cache/OrFixture.cs +++ b/src/DynamicData.Tests/Cache/OrFixture.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class OrFixture : OrFixtureBase { protected override IObservable> CreateObservable() @@ -28,7 +30,9 @@ protected override IObservable> CreateObservable() public abstract class OrFixtureBase : IDisposable { protected ISourceCache _source1; + protected ISourceCache _source2; + private readonly ChangeSetAggregator _results; protected OrFixtureBase() @@ -38,8 +42,6 @@ protected OrFixtureBase() _results = CreateObservable().AsAggregator(); } - protected abstract IObservable> CreateObservable(); - public void Dispose() { _source1.Dispose(); @@ -48,13 +50,15 @@ public void Dispose() } [Fact] - public void UpdatingOneSourceOnlyProducesResult() + public void RemovingFromOneDoesNotFromResult() { var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); + _source2.AddOrUpdate(person); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _source2.Remove(person); + _results.Messages.Count.Should().Be(1, "Should be 2 updates"); + _results.Data.Count.Should().Be(1, "Cache should have no items"); } [Fact] @@ -69,29 +73,29 @@ public void UpdatingBothProducesResultsAndDoesNotDuplicateTheMessage() } [Fact] - public void RemovingFromOneDoesNotFromResult() + public void UpdatingOneProducesOnlyOneUpdate() { var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - _source2.Remove(person); - _results.Messages.Count.Should().Be(1, "Should be 2 updates"); + var personUpdated = new Person("Adult1", 51); + _source2.AddOrUpdate(personUpdated); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); _results.Data.Count.Should().Be(1, "Cache should have no items"); + _results.Data.Items.First().Should().Be(personUpdated, "Should be updated person"); } [Fact] - public void UpdatingOneProducesOnlyOneUpdate() + public void UpdatingOneSourceOnlyProducesResult() { var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); - _source2.AddOrUpdate(person); - var personUpdated = new Person("Adult1", 51); - _source2.AddOrUpdate(personUpdated); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Data.Count.Should().Be(1, "Cache should have no items"); - _results.Data.Items.First().Should().Be(personUpdated, "Should be updated person"); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); } + + protected abstract IObservable> CreateObservable(); } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/PageFixture.cs b/src/DynamicData.Tests/Cache/PageFixture.cs index f4d66804d..a22ed3bc1 100644 --- a/src/DynamicData.Tests/Cache/PageFixture.cs +++ b/src/DynamicData.Tests/Cache/PageFixture.cs @@ -2,127 +2,130 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class PageFixture: IDisposable + public class PageFixture : IDisposable { - private readonly ISourceCache _source; private readonly PagedChangeSetAggregator _aggregators; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); private readonly IComparer _comparer; - private readonly ISubject> _sort; + + private readonly RandomPersonGenerator _generator = new(); + private readonly ISubject _pager; - public PageFixture() + private readonly ISubject> _sort; + + private readonly ISourceCache _source; + + public PageFixture() { - _source = new SourceCache(p=>p.Name); + _source = new SourceCache(p => p.Name); _comparer = SortExpressionComparer.Ascending(p => p.Name).ThenByAscending(p => p.Age); _sort = new BehaviorSubject>(_comparer); _pager = new BehaviorSubject(new PageRequest(1, 25)); - _aggregators = _source.Connect() - .Sort(_sort, resetThreshold: 200) - .Page(_pager) - .AsAggregator(); - } - - public void Dispose() - { - _source.Dispose(); - _aggregators.Dispose(); + _aggregators = _source.Connect().Sort(_sort, resetThreshold: 200).Page(_pager).AsAggregator(); } [Fact] - public void ReorderBelowThreshold() + public void ChangePage() { - var people = _generator.Take(50).ToArray(); + var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); + _pager.OnNext(new PageRequest(2, 25)); - var changed = SortExpressionComparer.Descending(p => p.Age).ThenByAscending(p => p.Name); - _sort.OnNext(changed); + var expectedResult = people.OrderBy(p => p, _comparer).Skip(25).Take(25).Select(p => new KeyValuePair(p.Name, p)).ToList(); + var actualResult = _aggregators.Messages[1].SortedItems.ToList(); - var expectedResult = people.OrderBy(p => p, changed).Take(25).Select(p => new KeyValuePair(p.Name, p)).ToList(); - var actualResult = _aggregators.Messages.Last().SortedItems.ToList(); actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void PageInitialBatch() + public void ChangePageSize() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); + _pager.OnNext(new PageRequest(1, 50)); - _aggregators.Data.Count.Should().Be(25, "Should be 25 people in the cache"); - _aggregators.Messages[0].Response.PageSize.Should().Be(25, "Page size should be 25"); - _aggregators.Messages[0].Response.Page.Should().Be(1, "Should be page 1"); - _aggregators.Messages[0].Response.Pages.Should().Be(4, "Should be page 4 pages"); + _aggregators.Messages[1].Response.Page.Should().Be(1, "Should be page 1"); - var expectedResult = people.OrderBy(p => p, _comparer).Take(25).Select(p => new KeyValuePair(p.Name, p)).ToList(); - var actualResult = _aggregators.Messages[0].SortedItems.ToList(); + var expectedResult = people.OrderBy(p => p, _comparer).Take(50).Select(p => new KeyValuePair(p.Name, p)).ToList(); + var actualResult = _aggregators.Messages[1].SortedItems.ToList(); actualResult.Should().BeEquivalentTo(expectedResult); } + public void Dispose() + { + _source.Dispose(); + _aggregators.Dispose(); + } + [Fact] - public void ChangePage() + public void PageGreaterThanNumberOfPagesAvailable() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - _pager.OnNext(new PageRequest(2, 25)); + _pager.OnNext(new PageRequest(10, 25)); - var expectedResult = people.OrderBy(p => p, _comparer).Skip(25).Take(25).Select(p => new KeyValuePair(p.Name, p)).ToList(); + _aggregators.Messages[1].Response.Page.Should().Be(4, "Page should move to the last page"); + + var expectedResult = people.OrderBy(p => p, _comparer).Skip(75).Take(25).Select(p => new KeyValuePair(p.Name, p)).ToList(); var actualResult = _aggregators.Messages[1].SortedItems.ToList(); actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void ChangePageSize() + public void PageInitialBatch() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - _pager.OnNext(new PageRequest(1, 50)); - _aggregators.Messages[1].Response.Page.Should().Be(1, "Should be page 1"); + _aggregators.Data.Count.Should().Be(25, "Should be 25 people in the cache"); + _aggregators.Messages[0].Response.PageSize.Should().Be(25, "Page size should be 25"); + _aggregators.Messages[0].Response.Page.Should().Be(1, "Should be page 1"); + _aggregators.Messages[0].Response.Pages.Should().Be(4, "Should be page 4 pages"); - var expectedResult = people.OrderBy(p => p, _comparer).Take(50).Select(p => new KeyValuePair(p.Name, p)).ToList(); - var actualResult = _aggregators.Messages[1].SortedItems.ToList(); + var expectedResult = people.OrderBy(p => p, _comparer).Take(25).Select(p => new KeyValuePair(p.Name, p)).ToList(); + var actualResult = _aggregators.Messages[0].SortedItems.ToList(); actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void PageGreaterThanNumberOfPagesAvailable() + public void ReorderBelowThreshold() { - var people = _generator.Take(100).ToArray(); + var people = _generator.Take(50).ToArray(); _source.AddOrUpdate(people); - _pager.OnNext(new PageRequest(10, 25)); - _aggregators.Messages[1].Response.Page.Should().Be(4, "Page should move to the last page"); - - var expectedResult = people.OrderBy(p => p, _comparer).Skip(75).Take(25).Select(p => new KeyValuePair(p.Name, p)).ToList(); - var actualResult = _aggregators.Messages[1].SortedItems.ToList(); + var changed = SortExpressionComparer.Descending(p => p.Age).ThenByAscending(p => p.Name); + _sort.OnNext(changed); + var expectedResult = people.OrderBy(p => p, changed).Take(25).Select(p => new KeyValuePair(p.Name, p)).ToList(); + var actualResult = _aggregators.Messages.Last().SortedItems.ToList(); actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void ThrowsForNegativeSizeParameters() + public void ThrowsForNegativePage() { - Assert.Throws(() => _pager.OnNext(new PageRequest(1, -1))); + Assert.Throws(() => _pager.OnNext(new PageRequest(-1, 1))); } [Fact] - public void ThrowsForNegativePage() + public void ThrowsForNegativeSizeParameters() { - Assert.Throws(() => _pager.OnNext(new PageRequest(-1, 1))); + Assert.Throws(() => _pager.OnNext(new PageRequest(1, -1))); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/QueryWhenChangedFixture.cs b/src/DynamicData.Tests/Cache/QueryWhenChangedFixture.cs index b4c9a5dfd..8d75beac0 100644 --- a/src/DynamicData.Tests/Cache/QueryWhenChangedFixture.cs +++ b/src/DynamicData.Tests/Cache/QueryWhenChangedFixture.cs @@ -1,48 +1,46 @@ -using DynamicData.Tests.Domain; -using Xunit; using System; + +using DynamicData.Tests.Domain; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - - public class QueryWhenChangedFixture: IDisposable + public class QueryWhenChangedFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; + private readonly ISourceCache _source; + public QueryWhenChangedFixture() { _source = new SourceCache(p => p.Name); _results = new ChangeSetAggregator(_source.Connect(p => p.Age > 20)); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] - public void ChangeInvokedOnSubscriptionIfItHasData() + public void ChangeInvokedOnNext() { bool invoked = false; + + var subscription = _source.Connect().QueryWhenChanged().Subscribe(x => invoked = true); + + invoked.Should().BeFalse(); + _source.AddOrUpdate(new Person("A", 1)); - var subscription = _source.Connect() - .QueryWhenChanged() - .Subscribe(x => invoked = true); invoked.Should().BeTrue(); + subscription.Dispose(); } [Fact] - public void ChangeInvokedOnNext() + public void ChangeInvokedOnNext_WithSelector() { bool invoked = false; - var subscription = _source.Connect() - .QueryWhenChanged() - .Subscribe(x => invoked = true); + var subscription = _source.Connect().QueryWhenChanged(query => query.Count).Subscribe(x => invoked = true); invoked.Should().BeFalse(); @@ -53,32 +51,29 @@ public void ChangeInvokedOnNext() } [Fact] - public void ChangeInvokedOnSubscriptionIfItHasData_WithSelector() + public void ChangeInvokedOnSubscriptionIfItHasData() { bool invoked = false; _source.AddOrUpdate(new Person("A", 1)); - var subscription = _source.Connect() - .QueryWhenChanged(query => query.Count) - .Subscribe(x => invoked = true); + var subscription = _source.Connect().QueryWhenChanged().Subscribe(x => invoked = true); invoked.Should().BeTrue(); subscription.Dispose(); } [Fact] - public void ChangeInvokedOnNext_WithSelector() + public void ChangeInvokedOnSubscriptionIfItHasData_WithSelector() { bool invoked = false; - - var subscription = _source.Connect() - .QueryWhenChanged(query => query.Count) - .Subscribe(x => invoked = true); - - invoked.Should().BeFalse(); - _source.AddOrUpdate(new Person("A", 1)); + var subscription = _source.Connect().QueryWhenChanged(query => query.Count).Subscribe(x => invoked = true); invoked.Should().BeTrue(); - subscription.Dispose(); } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/RefCountFixture.cs b/src/DynamicData.Tests/Cache/RefCountFixture.cs index c80821ffc..23b09da54 100644 --- a/src/DynamicData.Tests/Cache/RefCountFixture.cs +++ b/src/DynamicData.Tests/Cache/RefCountFixture.cs @@ -1,97 +1,93 @@ -using System.Reactive.Linq; -using DynamicData.Tests.Domain; -using Xunit; -using System; -using System.Threading.Tasks; +using System; using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; + +using DynamicData.Tests.Domain; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - - public class RefCountFixture: IDisposable + public class RefCountFixture : IDisposable { private readonly ISourceCache _source; - public RefCountFixture() + public RefCountFixture() { _source = new SourceCache(p => p.Key); } - public void Dispose() - { - _source.Dispose(); - } - [Fact] - public void ChainIsInvokedOnceForMultipleSubscribers() + public void CanResubscribe() { int created = 0; int disposals = 0; - //Some expensive transform (or chain of operations) - var longChain = _source.Connect() - .Transform(p => p) - .Do(_ => created++) - .Finally(() => disposals++) - .RefCount(); + // must have data so transform is invoked + _source.AddOrUpdate(new Person("Name", 10)); - var suscriber1 = longChain.Subscribe(); - var suscriber2 = longChain.Subscribe(); - var suscriber3 = longChain.Subscribe(); + // Some expensive transform (or chain of operations) + var longChain = _source.Connect().Transform(p => p).Do(_ => created++).Finally(() => disposals++).RefCount(); - _source.AddOrUpdate(new Person("Name", 10)); - suscriber1.Dispose(); - suscriber2.Dispose(); - suscriber3.Dispose(); + var subscriber = longChain.Subscribe(); + subscriber.Dispose(); - created.Should().Be(1); - disposals.Should().Be(1); + subscriber = longChain.Subscribe(); + subscriber.Dispose(); + + created.Should().Be(2); + disposals.Should().Be(2); } [Fact] - public void CanResubscribe() + public void ChainIsInvokedOnceForMultipleSubscribers() { int created = 0; int disposals = 0; - //must have data so transform is invoked - _source.AddOrUpdate(new Person("Name", 10)); + // Some expensive transform (or chain of operations) + var longChain = _source.Connect().Transform(p => p).Do(_ => created++).Finally(() => disposals++).RefCount(); - //Some expensive transform (or chain of operations) - var longChain = _source.Connect() - .Transform(p => p) - .Do(_ => created++) - .Finally(() => disposals++) - .RefCount(); + var subscriber1 = longChain.Subscribe(); + var subscriber2 = longChain.Subscribe(); + var subscriber3 = longChain.Subscribe(); - var subscriber = longChain.Subscribe(); - subscriber.Dispose(); + _source.AddOrUpdate(new Person("Name", 10)); + subscriber1.Dispose(); + subscriber2.Dispose(); + subscriber3.Dispose(); - subscriber = longChain.Subscribe(); - subscriber.Dispose(); + created.Should().Be(1); + disposals.Should().Be(1); + } - created.Should().Be(2); - disposals.Should().Be(2); + public void Dispose() + { + _source.Dispose(); } // This test is probabilistic, it could be cool to be able to prove RefCount's thread-safety // more accurately but I don't think that there is an easy way to do this. // At least this test can catch some bugs in the old implementation. - // [Fact] + // [Fact] private async Task IsHopefullyThreadSafe() { var refCount = _source.Connect().RefCount(); - await Task.WhenAll(Enumerable.Range(0, 100).Select(_ => - Task.Run(() => - { - for (int i = 0; i < 1000; ++i) - { - var subscription = refCount.Subscribe(); - subscription.Dispose(); - } - }))); + await Task.WhenAll( + Enumerable.Range(0, 100).Select( + _ => Task.Run( + () => + { + for (int i = 0; i < 1000; ++i) + { + var subscription = refCount.Subscribe(); + subscription.Dispose(); + } + }))); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/RightJoinFixture.cs b/src/DynamicData.Tests/Cache/RightJoinFixture.cs index 6dc07ae80..7df8f6024 100644 --- a/src/DynamicData.Tests/Cache/RightJoinFixture.cs +++ b/src/DynamicData.Tests/Cache/RightJoinFixture.cs @@ -1,49 +1,78 @@ using System; using System.Linq; + using DynamicData.Kernel; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - public class RightJoinFixture: IDisposable + public class RightJoinFixture : IDisposable { private readonly SourceCache _left; - private readonly SourceCache _right; + private readonly ChangeSetAggregator _result; - public RightJoinFixture() + private readonly SourceCache _right; + + public RightJoinFixture() { _left = new SourceCache(device => device.Name); _right = new SourceCache(device => device.Name); - _result = _left.Connect() - .RightJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(key, device, meta)) - .AsAggregator(); + _result = _left.Connect().RightJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(key, device, meta)).AsAggregator(); } [Fact] public void AddLeftOnly() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 0.Should().Be(_result.Data.Count); } + [Fact] + public void AddLetThenRight() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + 3.Should().Be(_result.Data.Count); + + _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); + } + [Fact] public void AddRightOnly() { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Lookup("Device1").HasValue.Should().BeTrue(); @@ -54,43 +83,54 @@ public void AddRightOnly() } [Fact] - public void AddLetThenRight() + public void AddRightThenLeft() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); } + public void Dispose() + { + _left.Dispose(); + _right.Dispose(); + _result.Dispose(); + } + [Fact] public void RemoveVarious() { - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); _right.Remove("Device3"); @@ -101,69 +141,50 @@ public void RemoveVarious() _result.Data.Lookup("Device1").HasValue.Should().BeTrue(); } - [Fact] - public void AddRightThenLeft() - { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); - - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); - - 3.Should().Be(_result.Data.Count); - - _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); - } - [Fact] public void UpdateRight() { - _right.Edit(innerCache => - { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); - }); - - _left.Edit(innerCache => - { - innerCache.AddOrUpdate(new Device("Device1")); - innerCache.AddOrUpdate(new Device("Device2")); - innerCache.AddOrUpdate(new Device("Device3")); - }); + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData("Device1")); + innerCache.AddOrUpdate(new DeviceMetaData("Device2")); + innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + }); + + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); 3.Should().Be(_result.Data.Count); _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); } - public void Dispose() - { - _left.Dispose(); - _right.Dispose(); - _result.Dispose(); - } - public class Device : IEquatable { - public string Name { get; } - public Device(string name) { Name = name; } - #region Equality Members + public string Name { get; } + + public static bool operator ==(Device left, Device right) + { + return Equals(left, right); + } + + public static bool operator !=(Device left, Device right) + { + return !Equals(left, right); + } - public bool Equals(Device other) + public bool Equals(Device? other) { if (ReferenceEquals(null, other)) { @@ -178,7 +199,7 @@ public bool Equals(Device other) return string.Equals(Name, other.Name); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -200,21 +221,9 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Name != null ? Name.GetHashCode() : 0); + return (Name is not null ? Name.GetHashCode() : 0); } - public static bool operator ==(Device left, Device right) - { - return Equals(left, right); - } - - public static bool operator !=(Device left, Device right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{Name}"; @@ -223,19 +232,27 @@ public override string ToString() public class DeviceMetaData : IEquatable { - public string Name { get; } - - public bool IsAutoConnect { get; } - public DeviceMetaData(string name, bool isAutoConnect = false) { Name = name; IsAutoConnect = isAutoConnect; } - #region Equality members + public bool IsAutoConnect { get; } + + public string Name { get; } - public bool Equals(DeviceMetaData other) + public static bool operator ==(DeviceMetaData left, DeviceMetaData right) + { + return Equals(left, right); + } + + public static bool operator !=(DeviceMetaData left, DeviceMetaData right) + { + return !Equals(left, right); + } + + public bool Equals(DeviceMetaData? other) { if (ReferenceEquals(null, other)) { @@ -250,7 +267,7 @@ public bool Equals(DeviceMetaData other) return string.Equals(Name, other.Name) && IsAutoConnect == other.IsAutoConnect; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -274,22 +291,10 @@ public override int GetHashCode() { unchecked { - return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ IsAutoConnect.GetHashCode(); + return ((Name is not null ? Name.GetHashCode() : 0) * 397) ^ IsAutoConnect.GetHashCode(); } } - public static bool operator ==(DeviceMetaData left, DeviceMetaData right) - { - return Equals(left, right); - } - - public static bool operator !=(DeviceMetaData left, DeviceMetaData right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"Metadata: {Name}. IsAutoConnect = {IsAutoConnect}"; @@ -298,10 +303,6 @@ public override string ToString() public class DeviceWithMetadata : IEquatable { - public string Key { get; } - public Optional Device { get; } - public DeviceMetaData MetaData { get; } - public DeviceWithMetadata(string key, Optional device, DeviceMetaData metaData) { Key = key; @@ -309,9 +310,23 @@ public DeviceWithMetadata(string key, Optional device, DeviceMetaData me MetaData = metaData; } - #region Equality members + public Optional Device { get; } + + public string Key { get; } + + public DeviceMetaData MetaData { get; } + + public static bool operator ==(DeviceWithMetadata left, DeviceWithMetadata right) + { + return Equals(left, right); + } + + public static bool operator !=(DeviceWithMetadata left, DeviceWithMetadata right) + { + return !Equals(left, right); + } - public bool Equals(DeviceWithMetadata other) + public bool Equals(DeviceWithMetadata? other) { if (ReferenceEquals(null, other)) { @@ -326,7 +341,7 @@ public bool Equals(DeviceWithMetadata other) return string.Equals(Key, other.Key); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -351,18 +366,6 @@ public override int GetHashCode() return Key?.GetHashCode() ?? 0; } - public static bool operator ==(DeviceWithMetadata left, DeviceWithMetadata right) - { - return Equals(left, right); - } - - public static bool operator !=(DeviceWithMetadata left, DeviceWithMetadata right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{Key}: {Device} ({MetaData})"; diff --git a/src/DynamicData.Tests/Cache/RightJoinManyFixture.cs b/src/DynamicData.Tests/Cache/RightJoinManyFixture.cs index 68b9bbbe7..66c91bf60 100644 --- a/src/DynamicData.Tests/Cache/RightJoinManyFixture.cs +++ b/src/DynamicData.Tests/Cache/RightJoinManyFixture.cs @@ -1,38 +1,52 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class RightJoinManyFixture: IDisposable + public class RightJoinManyFixture : IDisposable { private readonly SourceCache _people; + private readonly ChangeSetAggregator _result; - public RightJoinManyFixture() + public RightJoinManyFixture() { _people = new SourceCache(p => p.Name); //All children will be included whether there is a parent or not - _result = _people.Connect() - .RightJoinMany(_people.Connect(), pac => pac.ParentName, (personid, person, grouping) => new ParentAndChildren(personid, person, grouping.Items.Select(p => p).ToArray())) - .AsAggregator(); + _result = _people.Connect().RightJoinMany(_people.Connect(), pac => pac.ParentName, (personid, person, grouping) => new ParentAndChildren(personid, person, grouping.Items.Select(p => p).ToArray())).AsAggregator(); } - public void Dispose() + [Fact] + public void AddChild() { - _people.Dispose(); - _result.Dispose(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); + + _people.AddOrUpdate(people); + + var person11 = new Person("Person11", 100, parentName: "Person3"); + _people.AddOrUpdate(person11); + + var updatedPeople = people.Union(new[] { person11 }).ToArray(); + + AssertDataIsCorrectlyFormed(updatedPeople); } [Fact] public void AddLeftOnly() { - var people = Enumerable.Range(1, 10) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + var people = Enumerable.Range(1, 10).Select(i => new Person("Person" + i, i)).ToArray(); _people.AddOrUpdate(people); @@ -43,51 +57,53 @@ public void AddLeftOnly() [Fact] public void AddPeopleWithParents() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); AssertDataIsCorrectlyFormed(people); } + public void Dispose() + { + _people.Dispose(); + _result.Dispose(); + } + [Fact] - public void UpdateParent() + public void RemoveChild() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); - var current10 = people.Last(); - var person10 = new Person("Person10", 100, parentName: current10.ParentName); - _people.AddOrUpdate(person10); + var last = people.Last(); + _people.Remove(last); - var updatedPeople = people.Take(9).Union(new[] { person10 }).ToArray(); + var updatedPeople = people.Where(p => p.Name != last.Name).ToArray(); - AssertDataIsCorrectlyFormed(updatedPeople); + AssertDataIsCorrectlyFormed(updatedPeople, last.Name); } [Fact] public void UpdateChild() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); @@ -101,68 +117,45 @@ public void UpdateChild() } [Fact] - public void AddChild() + public void UpdateParent() { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); + var people = Enumerable.Range(1, 10).Select( + i => + { + string parent = "Person" + CalculateParent(i, 10); + return new Person("Person" + i, i, parentName: parent); + }).ToArray(); _people.AddOrUpdate(people); - var person11 = new Person("Person11", 100, parentName: "Person3"); - _people.AddOrUpdate(person11); + var current10 = people.Last(); + var person10 = new Person("Person10", 100, parentName: current10.ParentName); + _people.AddOrUpdate(person10); - var updatedPeople = people.Union(new[] { person11 }).ToArray(); + var updatedPeople = people.Take(9).Union(new[] { person10 }).ToArray(); AssertDataIsCorrectlyFormed(updatedPeople); } - [Fact] - public void RemoveChild() - { - var people = Enumerable.Range(1, 10) - .Select(i => - { - string parent = "Person" + CalculateParent(i, 10); - return new Person("Person" + i, i, parentName: parent); - }) - .ToArray(); - - _people.AddOrUpdate(people); - - var last = people.Last(); - _people.Remove(last); - - var updatedPeople = people.Where(p => p.Name != last.Name).ToArray(); - - AssertDataIsCorrectlyFormed(updatedPeople, last.Name); - } - private void AssertDataIsCorrectlyFormed(Person[] allPeople, params string[] missingParents) { - var grouped = allPeople.GroupBy(p => p.ParentName) - .Where(p => p.Any() && !missingParents.Contains(p.Key)) - .AsArray(); + var grouped = allPeople.GroupBy(p => p.ParentName).Where(p => p.Any() && !missingParents.Contains(p.Key)).AsArray(); _result.Data.Count.Should().Be(grouped.Length); - grouped.ForEach(grouping => - { - if (missingParents.Length > 0 && missingParents.Contains(grouping.Key)) - { - return; - } + grouped.ForEach( + grouping => + { + if (missingParents.Length > 0 && missingParents.Contains(grouping.Key)) + { + return; + } - var result = _result.Data.Lookup(grouping.Key) - .ValueOrThrow(() => new Exception("Missing result for " + grouping.Key)); + var result = _result.Data.Lookup(grouping.Key).ValueOrThrow(() => new Exception("Missing result for " + grouping.Key)); - var children = result.Children; - children.Should().BeEquivalentTo(grouping); - }); + var children = result.Children; + children.Should().BeEquivalentTo(grouping); + }); } private int CalculateParent(int index, int totalPeople) @@ -184,6 +177,5 @@ private int CalculateParent(int index, int totalPeople) return index + 1; } - } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/SizeLimitFixture.cs b/src/DynamicData.Tests/Cache/SizeLimitFixture.cs index 8d6d283e6..4872ecf72 100644 --- a/src/DynamicData.Tests/Cache/SizeLimitFixture.cs +++ b/src/DynamicData.Tests/Cache/SizeLimitFixture.cs @@ -1,24 +1,30 @@ using System; using System.Linq; using System.Reactive.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { - - public class SizeLimitFixture: IDisposable + public class SizeLimitFixture : IDisposable { - private readonly ISourceCache _source; + private readonly RandomPersonGenerator _generator = new(); + private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; + private readonly IDisposable _sizeLimiter; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + private readonly ISourceCache _source; - public SizeLimitFixture() + public SizeLimitFixture() { _scheduler = new TestScheduler(); _source = new SourceCache(p => p.Key); @@ -26,11 +32,15 @@ public SizeLimitFixture() _results = _source.Connect().AsAggregator(); } - public void Dispose() + [Fact] + public void Add() { - _sizeLimiter.Dispose(); - _source.Dispose(); - _results.Dispose(); + var person = _generator.Take(1).First(); + _source.AddOrUpdate(person); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Items.First().Should().Be(person, "Should be same person"); } [Fact] @@ -73,47 +83,18 @@ public void AddMoreThanLimitInBatched() _results.Messages[2].Removes.Should().Be(10, "Should be 10 removes in the third update"); } - [Fact] - public void Add() - { - var person = _generator.Take(1).First(); - _source.AddOrUpdate(person); - - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - _results.Data.Items.First().Should().Be(person, "Should be same person"); - } - - [Fact] - public void ThrowsIfSizeLimitIsZero() - { - // Initialise(); - Assert.Throws(() => new SourceCache(p => p.Key).LimitSizeTo(0)); - } - - [Fact] - public void OnCompleteIsInvokedWhenSourceIsDisposed() + public void Dispose() { - bool completed = false; - - var subscriber = _source.LimitSizeTo(10) - .Finally(() => completed = true) - .Subscribe(updates => { Console.WriteLine(); }); - + _sizeLimiter.Dispose(); _source.Dispose(); - - completed.Should().BeTrue(); + _results.Dispose(); } [Fact] public void InvokeLimitSizeToWhenOverLimit() { bool removesTriggered = false; - var subscriber = _source.LimitSizeTo(10, _scheduler) - .Subscribe(removes => - { - removesTriggered = true; - }); + var subscriber = _source.LimitSizeTo(10, _scheduler).Subscribe(removes => { removesTriggered = true; }); _source.AddOrUpdate(_generator.Take(10).ToArray()); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(150).Ticks); @@ -133,5 +114,24 @@ public void InvokeLimitSizeToWhenOverLimit() subscriber.Dispose(); } + + [Fact] + public void OnCompleteIsInvokedWhenSourceIsDisposed() + { + bool completed = false; + + var subscriber = _source.LimitSizeTo(10).Finally(() => completed = true).Subscribe(updates => { Console.WriteLine(); }); + + _source.Dispose(); + + completed.Should().BeTrue(); + } + + [Fact] + public void ThrowsIfSizeLimitIsZero() + { + // Initialise(); + Assert.Throws(() => new SourceCache(p => p.Key).LimitSizeTo(0)); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/SortFixture.cs b/src/DynamicData.Tests/Cache/SortFixture.cs index b978143c8..578cc5790 100644 --- a/src/DynamicData.Tests/Cache/SortFixture.cs +++ b/src/DynamicData.Tests/Cache/SortFixture.cs @@ -5,10 +5,13 @@ using System.Linq; using System.Reactive; using System.Reactive.Subjects; + using DynamicData.Binding; using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; #endregion @@ -17,20 +20,237 @@ namespace DynamicData.Tests.Cache { public class SortFixtureWithReorder : IDisposable { - private readonly ISourceCache _source; - private readonly SortedChangeSetAggregator _results; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); private readonly IComparer _comparer; + private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly SortedChangeSetAggregator _results; + + private readonly ISourceCache _source; + public SortFixtureWithReorder() { _comparer = SortExpressionComparer.Ascending(p => p.Age).ThenByAscending(p => p.Name); _source = new SourceCache(p => p.Key); - _results = new SortedChangeSetAggregator - ( - _source.Connect().Sort(_comparer) - ); + _results = new SortedChangeSetAggregator(_source.Connect().Sort(_comparer)); + } + + [Fact] + public void AppendAtBeginning() + { + var people = _generator.Take(100).ToArray(); + _source.AddOrUpdate(people); + + //create age 0 to ensure it is inserted first + var insert = new Person("_Aaron", 0); + + _source.AddOrUpdate(insert); + + _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("_Aaron"); + + indexedItem.HasValue.Should().BeTrue(); + indexedItem.Value.Index.Should().Be(0, "Inserted item should have index of zero"); + } + + [Fact] + public void AppendAtEnd() + { + var people = _generator.Take(100).ToArray(); + _source.AddOrUpdate(people); + + //create age 0 to ensure it is inserted first + var insert = new Person("zzzzz", 1000); + + _source.AddOrUpdate(insert); + + _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("zzzzz"); + + indexedItem.HasValue.Should().BeTrue(); + + var list = _results.Messages[1].SortedItems.ToList(); + var sortedResult = list.OrderBy(p => _comparer).ToList(); + list.Should().BeEquivalentTo(sortedResult); + } + + [Fact] + public void AppendInMiddle() + { + var people = _generator.Take(100).ToArray(); + _source.AddOrUpdate(people); + + //create age 0 to ensure it is inserted first + var insert = new Person("Marvin", 50); + + _source.AddOrUpdate(insert); + + _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("Marvin"); + + indexedItem.HasValue.Should().BeTrue(); + + var list = _results.Messages[1].SortedItems.ToList(); + var sortedResult = list.OrderBy(p => _comparer).ToList(); + list.Should().BeEquivalentTo(sortedResult); + } + + [Fact] + public void BatchUpdate1() + { + var people = _generator.Take(10).ToArray(); + _source.AddOrUpdate(people); + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + + var toupdate = people[3]; + + _source.Edit( + updater => + { + updater.Remove(people[0].Key); + updater.Remove(people[1].Key); + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); + updater.Remove(people[7]); + }); + + var adaptor = new SortedObservableCollectionAdaptor(); + + adaptor.Adapt(_results.Messages.Last(), list); + + var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); + list.Should().BeEquivalentTo(shouldbe); + } + + [Fact] + public void BatchUpdate2() + { + var people = _generator.Take(10).ToArray(); + _source.AddOrUpdate(people); + + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + + var toupdate = people[3]; + + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); + updater.AddOrUpdate(new Person("Mr", "Z", 50, "M")); + }); + + var adaptor = new SortedObservableCollectionAdaptor(); + + adaptor.Adapt(_results.Messages.Last(), list); + + var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); + list.Should().BeEquivalentTo(shouldbe); + } + + [Fact] + public void BatchUpdate3() + { + var people = _generator.Take(10).ToArray(); + _source.AddOrUpdate(people); + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + + var toupdate = people[7]; + + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); + updater.AddOrUpdate(new Person("Mr", "A", 10, "M")); + updater.AddOrUpdate(new Person("Mr", "B", 40, "M")); + updater.AddOrUpdate(new Person("Mr", "C", 70, "M")); + }); + + var adaptor = new SortedObservableCollectionAdaptor(); + + adaptor.Adapt(_results.Messages.Last(), list); + + var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); + list.Should().BeEquivalentTo(shouldbe); + } + + [Fact] + public void BatchUpdate4() + { + var people = _generator.Take(10).ToArray(); + _source.AddOrUpdate(people); + + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + + var toupdate = people[3]; + + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); + updater.AddOrUpdate(new Person("Mr", "A", 10, "M")); + updater.Remove(people[5]); + updater.AddOrUpdate(new Person("Mr", "C", 70, "M")); + }); + + var adaptor = new SortedObservableCollectionAdaptor(); + + adaptor.Adapt(_results.Messages.Last(), list); + + var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); + list.Should().BeEquivalentTo(shouldbe); + } + + [Fact] + public void BatchUpdate6() + { + var people = _generator.Take(10).ToArray(); + _source.AddOrUpdate(people); + + _source.Edit( + updater => + { + updater.Clear(); + updater.AddOrUpdate(_generator.Take(10).ToArray()); + updater.Clear(); + }); + + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + + var adaptor = new SortedObservableCollectionAdaptor(); + + adaptor.Adapt(_results.Messages.Last(), list); + + var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); + list.Should().BeEquivalentTo(shouldbe); + } + + [Fact] + public void BatchUpdateWhereUpdateMovesTheIndexDown() + { + var people = _generator.Take(10).ToArray(); + _source.AddOrUpdate(people); + + var toupdate = people[3]; + + _source.Edit( + updater => + { + updater.Remove(people[0].Key); + updater.Remove(people[1].Key); + + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age + 50)); + + updater.AddOrUpdate(_generator.Take(2)); + + updater.Remove(people[7]); + }); + + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + var adaptor = new SortedObservableCollectionAdaptor(); + adaptor.Adapt(_results.Messages.Last(), list); + + var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); + list.Should().BeEquivalentTo(shouldbe); } public void Dispose() @@ -44,9 +264,7 @@ public void DoesNotThrow1() { var cache = new SourceCache(d => d.Id); var sortPump = new Subject(); - var disposable = cache.Connect() - .Sort(SortExpressionComparer.Ascending(d => d.Id), sortPump) - .Subscribe(); + var disposable = cache.Connect().Sort(SortExpressionComparer.Ascending(d => d.Id), sortPump).Subscribe(); disposable.Dispose(); } @@ -55,76 +273,90 @@ public void DoesNotThrow1() public void DoesNotThrow2() { var cache = new SourceCache(d => d.Id); - var disposable = cache.Connect() - .Sort(new BehaviorSubject>(SortExpressionComparer.Ascending(d => d.Id))) - .Subscribe(); + var disposable = cache.Connect().Sort(new BehaviorSubject>(SortExpressionComparer.Ascending(d => d.Id))).Subscribe(); disposable.Dispose(); } - public class Data + [Fact] + public void InlineUpdateProducesAReplace() { - public Data(int id, string value) - { - Id = id; - Value = value; - } + var people = _generator.Take(10).ToArray(); + _source.AddOrUpdate(people); + var toupdate = people[3]; - public int Id { get; } - public string Value { get; } + _source.AddOrUpdate(new Person(toupdate.Name, toupdate.Age + 1)); + + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + var adaptor = new SortedObservableCollectionAdaptor(); + adaptor.Adapt(_results.Messages.Last(), list); + + var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); + list.Should().BeEquivalentTo(shouldbe); } - public class TestString : IEquatable + [Fact] + public void RemoveFirst() { - private readonly string _name; + var people = _generator.Take(100).ToArray(); + _source.AddOrUpdate(people); - public TestString(string name) - { - _name = name; - } + //create age 0 to ensure it is inserted first + var remove = _results.Messages[0].SortedItems.First(); - public static implicit operator TestString(string source) - { - return new TestString(source); - } + _source.Remove(remove.Key); - public static implicit operator string(TestString source) - { - return source._name; - } + _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + //TODO: fixed Text + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); + indexedItem.HasValue.Should().BeFalse(); - public bool Equals(TestString other) - { - return StringComparer.OrdinalIgnoreCase.Equals(_name, other._name); - } + var list = _results.Messages[1].SortedItems.ToList(); + var sortedResult = list.OrderBy(p => _comparer).ToList(); + list.Should().BeEquivalentTo(sortedResult); + } - public override bool Equals(object obj) - { - return Equals(obj as TestString); - } + [Fact] + public void RemoveFromEnd() + { + var people = _generator.Take(100).ToArray(); + _source.AddOrUpdate(people); - public override int GetHashCode() - { - return StringComparer.OrdinalIgnoreCase.GetHashCode(_name); - } + //create age 0 to ensure it is inserted first + var remove = _results.Messages[0].SortedItems.Last(); + + _source.Remove(remove.Key); + + _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); + indexedItem.HasValue.Should().BeFalse(); + + var list = _results.Messages[1].SortedItems.ToList(); + var sortedResult = list.OrderBy(p => _comparer).ToList(); + list.Should().BeEquivalentTo(sortedResult); } - public class ViewModel + [Fact] + public void RemoveFromMiddle() { - public string Name { get; } + var people = _generator.Take(100).ToArray(); + _source.AddOrUpdate(people); - public ViewModel(string name) - { - Name = name; - } + //create age 0 to ensure it is inserted first + var remove = _results.Messages[0].SortedItems.Skip(50).First(); - public class Comparer : IComparer - { - public int Compare(ViewModel x, ViewModel y) - { - return StringComparer.OrdinalIgnoreCase.Compare(x.Name, y.Name); - } - } + _source.Remove(remove.Key); + + _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + + //TODO: fixed Text + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); + indexedItem.HasValue.Should().BeFalse(); + + var list = _results.Messages[1].SortedItems.ToList(); + var sortedResult = list.OrderBy(p => _comparer).ToList(); + list.Should().BeEquivalentTo(sortedResult); } [Fact] @@ -134,19 +366,16 @@ public void SortAfterFilter() var filterSubject = new BehaviorSubject>(p => true); - var agg = new SortedChangeSetAggregator(source.Connect() - .Filter(filterSubject) - .Group(x => (TestString)x.Key) - .Transform(x => new ViewModel(x.Key)) - .Sort(new ViewModel.Comparer())); + var agg = new SortedChangeSetAggregator(source.Connect().Filter(filterSubject).Group(x => (TestString)x.Key).Transform(x => new ViewModel(x.Key)).Sort(new ViewModel.Comparer())); - source.Edit(x => - { - x.AddOrUpdate(new Person("A", 1, "F")); - x.AddOrUpdate(new Person("a", 1, "M")); - x.AddOrUpdate(new Person("B", 1, "F")); - x.AddOrUpdate(new Person("b", 1, "M")); - }); + source.Edit( + x => + { + x.AddOrUpdate(new Person("A", 1, "F")); + x.AddOrUpdate(new Person("a", 1, "M")); + x.AddOrUpdate(new Person("B", 1, "F")); + x.AddOrUpdate(new Person("b", 1, "M")); + }); filterSubject.OnNext(p => p.Name.Equals("a", StringComparison.OrdinalIgnoreCase)); } @@ -158,19 +387,16 @@ public void SortAfterFilterList() var filterSubject = new BehaviorSubject>(p => true); - var agg = source.Connect() - .Filter(filterSubject) - .Transform(x => new ViewModel(x.Name)) - .Sort(new ViewModel.Comparer()) - .AsAggregator(); + var agg = source.Connect().Filter(filterSubject).Transform(x => new ViewModel(x.Name)).Sort(new ViewModel.Comparer()).AsAggregator(); - source.Edit(x => - { - x.Add(new Person("A", 1, "F")); - x.Add(new Person("a", 1, "M")); - x.Add(new Person("B", 1, "F")); - x.Add(new Person("b", 1, "M")); - }); + source.Edit( + x => + { + x.Add(new Person("A", 1, "F")); + x.Add(new Person("a", 1, "M")); + x.Add(new Person("B", 1, "F")); + x.Add(new Person("b", 1, "M")); + }); filterSubject.OnNext(p => p.Name.Equals("a", StringComparison.OrdinalIgnoreCase)); } @@ -190,190 +416,211 @@ public void SortInitialBatch() } [Fact] - public void AppendAtBeginning() + public void UpdateFirst() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - //create age 0 to ensure it is inserted first - var insert = new Person("_Aaron", 0); - - _source.AddOrUpdate(insert); + var toupdate = _results.Messages[0].SortedItems.First().Value; + var update = new Person(toupdate.Name, toupdate.Age + 5); - _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("_Aaron"); + _source.AddOrUpdate(update); + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + //TODO: fixed Text + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); indexedItem.HasValue.Should().BeTrue(); - indexedItem.Value.Index.Should().Be(0, "Inserted item should have index of zero"); + ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); + var list = _results.Messages[1].SortedItems.ToList(); + var sortedResult = list.OrderBy(p => _comparer).ToList(); + list.Should().BeEquivalentTo(sortedResult); } [Fact] - public void AppendInMiddle() + public void UpdateLast() { + //TODO: fixed Text + var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - //create age 0 to ensure it is inserted first - var insert = new Person("Marvin", 50); + var toupdate = _results.Messages[0].SortedItems.Last().Value; + var update = new Person(toupdate.Name, toupdate.Age + 5); - _source.AddOrUpdate(insert); + _source.AddOrUpdate(update); - _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("Marvin"); + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); indexedItem.HasValue.Should().BeTrue(); - + ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); var list = _results.Messages[1].SortedItems.ToList(); var sortedResult = list.OrderBy(p => _comparer).ToList(); list.Should().BeEquivalentTo(sortedResult); } [Fact] - public void AppendAtEnd() + public void UpdateMiddle() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - //create age 0 to ensure it is inserted first - var insert = new Person("zzzzz", 1000); + var toupdate = _results.Messages[0].SortedItems.Skip(50).First().Value; + var update = new Person(toupdate.Name, toupdate.Age + 5); - _source.AddOrUpdate(insert); + _source.AddOrUpdate(update); - _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("zzzzz"); + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - indexedItem.HasValue.Should().BeTrue(); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); + indexedItem.HasValue.Should().BeTrue(); + ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); var list = _results.Messages[1].SortedItems.ToList(); var sortedResult = list.OrderBy(p => _comparer).ToList(); list.Should().BeEquivalentTo(sortedResult); } - [Fact] - public void RemoveFirst() + public class Data { - var people = _generator.Take(100).ToArray(); - _source.AddOrUpdate(people); - - //create age 0 to ensure it is inserted first - var remove = _results.Messages[0].SortedItems.First(); - - _source.Remove(remove.Key); + public Data(int id, string value) + { + Id = id; + Value = value; + } - _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); - //TODO: fixed Text - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); - indexedItem.HasValue.Should().BeFalse(); + public int Id { get; } - var list = _results.Messages[1].SortedItems.ToList(); - var sortedResult = list.OrderBy(p => _comparer).ToList(); - list.Should().BeEquivalentTo(sortedResult); + public string Value { get; } } - [Fact] - public void RemoveFromMiddle() + public class TestString : IEquatable { - var people = _generator.Take(100).ToArray(); - _source.AddOrUpdate(people); + private readonly string _name; - //create age 0 to ensure it is inserted first - var remove = _results.Messages[0].SortedItems.Skip(50).First(); + public TestString(string name) + { + _name = name; + } - _source.Remove(remove.Key); + public static implicit operator TestString(string source) + { + return new TestString(source); + } - _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + public static implicit operator string(TestString source) + { + return source._name; + } - //TODO: fixed Text - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); - indexedItem.HasValue.Should().BeFalse(); + public bool Equals(TestString? other) + { + return StringComparer.OrdinalIgnoreCase.Equals(_name, other?._name); + } - var list = _results.Messages[1].SortedItems.ToList(); - var sortedResult = list.OrderBy(p => _comparer).ToList(); - list.Should().BeEquivalentTo(sortedResult); + public override bool Equals(object? obj) + { + return obj is TestString value && Equals(value); + } + + public override int GetHashCode() + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(_name); + } + } + + public class ViewModel + { + public ViewModel(string name) + { + Name = name; + } + + public string Name { get; } + + public class Comparer : IComparer + { + public int Compare(ViewModel? x, ViewModel? y) + { + return StringComparer.OrdinalIgnoreCase.Compare(x?.Name, y?.Name); + } + } } + } - [Fact] - public void RemoveFromEnd() - { - var people = _generator.Take(100).ToArray(); - _source.AddOrUpdate(people); + public class SortFixture : IDisposable + { + private readonly IComparer _comparer; - //create age 0 to ensure it is inserted first - var remove = _results.Messages[0].SortedItems.Last(); + private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); - _source.Remove(remove.Key); + private readonly SortedChangeSetAggregator _results; - _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + private readonly ISourceCache _source; - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); - indexedItem.HasValue.Should().BeFalse(); + public SortFixture() + { + _comparer = SortExpressionComparer.Ascending(p => p.Age).ThenByAscending(p => p.Name); - var list = _results.Messages[1].SortedItems.ToList(); - var sortedResult = list.OrderBy(p => _comparer).ToList(); - list.Should().BeEquivalentTo(sortedResult); + _source = new SourceCache(p => p.Key); + _results = new SortedChangeSetAggregator(_source.Connect().Sort(_comparer)); } [Fact] - public void UpdateFirst() + public void AppendAtBeginning() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - var toupdate = _results.Messages[0].SortedItems.First().Value; - var update = new Person(toupdate.Name, toupdate.Age + 5); + //create age 0 to ensure it is inserted first + var insert = new Person("_Aaron", 0); - _source.AddOrUpdate(update); + _source.AddOrUpdate(insert); + + _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("_Aaron"); - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - //TODO: fixed Text - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); indexedItem.HasValue.Should().BeTrue(); - ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); - var list = _results.Messages[1].SortedItems.ToList(); - var sortedResult = list.OrderBy(p => _comparer).ToList(); - list.Should().BeEquivalentTo(sortedResult); + indexedItem.Value.Index.Should().Be(0, "Inserted item should have index of zero"); } [Fact] - public void UpdateMiddle() + public void AppendAtEnd() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - var toupdate = _results.Messages[0].SortedItems.Skip(50).First().Value; - var update = new Person(toupdate.Name, toupdate.Age + 5); - - _source.AddOrUpdate(update); + //create age 0 to ensure it is inserted first + var insert = new Person("zzzzz", 1000); - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + _source.AddOrUpdate(insert); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); + _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("zzzzz"); indexedItem.HasValue.Should().BeTrue(); - ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); + var list = _results.Messages[1].SortedItems.ToList(); var sortedResult = list.OrderBy(p => _comparer).ToList(); list.Should().BeEquivalentTo(sortedResult); } [Fact] - public void UpdateLast() + public void AppendInMiddle() { - //TODO: fixed Text - var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - var toupdate = _results.Messages[0].SortedItems.Last().Value; - var update = new Person(toupdate.Name, toupdate.Age + 5); + //create age 0 to ensure it is inserted first + var insert = new Person("Marvin", 50); - _source.AddOrUpdate(update); + _source.AddOrUpdate(insert); - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); + _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("Marvin"); indexedItem.HasValue.Should().BeTrue(); - ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); + var list = _results.Messages[1].SortedItems.ToList(); var sortedResult = list.OrderBy(p => _comparer).ToList(); list.Should().BeEquivalentTo(sortedResult); @@ -388,13 +635,14 @@ public void BatchUpdate1() var toupdate = people[3]; - _source.Edit(updater => - { - updater.Remove(people[0].Key); - updater.Remove(people[1].Key); - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); - updater.Remove(people[7]); - }); + _source.Edit( + updater => + { + updater.Remove(people[0].Key); + updater.Remove(people[1].Key); + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); + updater.Remove(people[7]); + }); var adaptor = new SortedObservableCollectionAdaptor(); @@ -405,27 +653,24 @@ public void BatchUpdate1() } [Fact] - public void BatchUpdateWhereUpdateMovesTheIndexDown() + public void BatchUpdate2() { var people = _generator.Take(10).ToArray(); _source.AddOrUpdate(people); - var toupdate = people[3]; - - _source.Edit(updater => - { - updater.Remove(people[0].Key); - updater.Remove(people[1].Key); - - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age + 50)); + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - updater.AddOrUpdate(_generator.Take(2)); + var toupdate = people[3]; - updater.Remove(people[7]); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); + updater.AddOrUpdate(new Person("Mr", "Z", 50, "M")); + }); - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); var adaptor = new SortedObservableCollectionAdaptor(); + adaptor.Adapt(_results.Messages.Last(), list); var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); @@ -433,45 +678,48 @@ public void BatchUpdateWhereUpdateMovesTheIndexDown() } [Fact] - public void BatchUpdate2() + public void BatchUpdate3() { var people = _generator.Take(10).ToArray(); _source.AddOrUpdate(people); - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - var toupdate = people[3]; + var toupdate = people[7]; - _source.Edit(updater => - { - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); - updater.AddOrUpdate(new Person("Mr", "Z", 50, "M")); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); + updater.AddOrUpdate(new Person("Mr", "A", 10, "M")); + updater.AddOrUpdate(new Person("Mr", "B", 40, "M")); + updater.AddOrUpdate(new Person("Mr", "C", 70, "M")); + }); var adaptor = new SortedObservableCollectionAdaptor(); adaptor.Adapt(_results.Messages.Last(), list); - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); list.Should().BeEquivalentTo(shouldbe); } [Fact] - public void BatchUpdate3() + public void BatchUpdate4() { var people = _generator.Take(10).ToArray(); _source.AddOrUpdate(people); + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - var toupdate = people[7]; + var toupdate = people[3]; - _source.Edit(updater => - { - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); - updater.AddOrUpdate(new Person("Mr", "A", 10, "M")); - updater.AddOrUpdate(new Person("Mr", "B", 40, "M")); - updater.AddOrUpdate(new Person("Mr", "C", 70, "M")); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); + updater.AddOrUpdate(new Person("Mr", "A", 10, "M")); + updater.Remove(people[5]); + updater.AddOrUpdate(new Person("Mr", "C", 70, "M")); + }); var adaptor = new SortedObservableCollectionAdaptor(); @@ -482,22 +730,20 @@ public void BatchUpdate3() } [Fact] - public void BatchUpdate4() + public void BatchUpdate6() { var people = _generator.Take(10).ToArray(); _source.AddOrUpdate(people); - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - - var toupdate = people[3]; + _source.Edit( + updater => + { + updater.Clear(); + updater.AddOrUpdate(_generator.Take(10).ToArray()); + updater.Clear(); + }); - _source.Edit(updater => - { - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); - updater.AddOrUpdate(new Person("Mr", "A", 10, "M")); - updater.Remove(people[5]); - updater.AddOrUpdate(new Person("Mr", "C", 70, "M")); - }); + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); var adaptor = new SortedObservableCollectionAdaptor(); @@ -508,36 +754,56 @@ public void BatchUpdate4() } [Fact] - public void BatchUpdate6() + public void BatchUpdateShiftingIndicies() { - var people = _generator.Take(10).ToArray(); - _source.AddOrUpdate(people); + var testData = new[] + { + new Person("A", 3), + new Person("B", 5), + new Person("C", 7), + new Person("D", 8), + new Person("E", 10), + new Person("F", 12), + new Person("G", 14) + }; + _source.AddOrUpdate(testData); + var list = new ObservableCollectionExtended(testData.OrderBy(p => p, _comparer)); - _source.Edit(updater => - { - updater.Clear(); - updater.AddOrUpdate(_generator.Take(10).ToArray()); - updater.Clear(); - }); + var toUpdate1 = testData[0]; + var toUpdate2 = testData[3]; - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + _source.Edit( + updater => + { + updater.AddOrUpdate(new Person(toUpdate1.Name, 6)); + updater.AddOrUpdate(new Person(toUpdate2.Name, 2)); + }); var adaptor = new SortedObservableCollectionAdaptor(); adaptor.Adapt(_results.Messages.Last(), list); - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); list.Should().BeEquivalentTo(shouldbe); } [Fact] - public void InlineUpdateProducesAReplace() + public void BatchUpdateWhereUpdateMovesTheIndexDown() { var people = _generator.Take(10).ToArray(); _source.AddOrUpdate(people); + var toupdate = people[3]; - _source.AddOrUpdate(new Person(toupdate.Name, toupdate.Age + 1)); + _source.Edit( + updater => + { + updater.Remove(people[0].Key); + updater.Remove(people[1].Key); + + updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age + 50)); + + updater.Remove(people[7]); + }); var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); var adaptor = new SortedObservableCollectionAdaptor(); @@ -546,25 +812,6 @@ public void InlineUpdateProducesAReplace() var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); list.Should().BeEquivalentTo(shouldbe); } - } - - public class SortFixture: IDisposable - { - private readonly ISourceCache _source; - private readonly SortedChangeSetAggregator _results; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); - private readonly IComparer _comparer; - - public SortFixture() - { - _comparer = SortExpressionComparer.Ascending(p => p.Age).ThenByAscending(p=>p.Name); - - _source = new SourceCache(p => p.Key); - _results = new SortedChangeSetAggregator - ( - _source.Connect().Sort(_comparer) - ); - } public void Dispose() { @@ -577,9 +824,7 @@ public void DoesNotThrow1() { var cache = new SourceCache(d => d.Id); var sortPump = new Subject(); - var disposable = cache.Connect() - .Sort(SortExpressionComparer.Ascending(d => d.Id), sortPump) - .Subscribe(); + var disposable = cache.Connect().Sort(SortExpressionComparer.Ascending(d => d.Id), sortPump).Subscribe(); disposable.Dispose(); } @@ -588,194 +833,43 @@ public void DoesNotThrow1() public void DoesNotThrow2() { var cache = new SourceCache(d => d.Id); - var disposable = cache.Connect() - .Sort(new BehaviorSubject>(SortExpressionComparer.Ascending(d => d.Id))) - .Subscribe(); - - disposable.Dispose(); - } - - public class Data - { - public Data(int id, string value) - { - Id = id; - Value = value; - } - - public int Id { get; } - public string Value { get; } - } - - public class TestString : IEquatable - { - private readonly string _name; - - public TestString(string name) - { - _name = name; - } - - public static implicit operator TestString(string source) - { - return new TestString(source); - } - - public static implicit operator string (TestString source) - { - return source._name; - } - - public bool Equals(TestString other) - { - return StringComparer.OrdinalIgnoreCase.Equals(_name, other._name); - } - - public override bool Equals(object obj) - { - return Equals(obj as TestString); - } - - public override int GetHashCode() - { - return StringComparer.OrdinalIgnoreCase.GetHashCode(_name); - } - } - - public class ViewModel - { - public string Name { get; set; } - - public ViewModel(string name) - { - this.Name = name; - } - - public class Comparer : IComparer - { - public int Compare(ViewModel x, ViewModel y) - { - return StringComparer.OrdinalIgnoreCase.Compare(x.Name, y.Name); - } - } - } - - [Fact] - public void SortAfterFilter() - { - var source = new SourceCache(p => p.Key); - - var filterSubject = new BehaviorSubject>(p => true); - - var agg = new SortedChangeSetAggregator(source.Connect() - .Filter(filterSubject) - .Group(x => (TestString)x.Key) - .Transform(x => new ViewModel(x.Key)) - .Sort(new ViewModel.Comparer())); - - source.Edit(x => - { - x.AddOrUpdate(new Person("A", 1, "F")); - x.AddOrUpdate(new Person("a", 1, "M")); - x.AddOrUpdate(new Person("B", 1, "F")); - x.AddOrUpdate(new Person("b", 1, "M")); - }); - - filterSubject.OnNext(p => p.Name.Equals("a", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void SortAfterFilterList() - { - var source = new SourceList(); - - var filterSubject = new BehaviorSubject>(p => true); - - var agg = source.Connect() - .Filter(filterSubject) - .Transform(x => new ViewModel(x.Name)) - .Sort(new ViewModel.Comparer()) - .AsAggregator(); - - source.Edit(x => - { - x.Add(new Person("A", 1, "F")); - x.Add(new Person("a", 1, "M")); - x.Add(new Person("B", 1, "F")); - x.Add(new Person("b", 1, "M")); - }); - - filterSubject.OnNext(p => p.Name.Equals("a", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void SortInitialBatch() - { - var people = _generator.Take(100).ToArray(); - _source.AddOrUpdate(people); - - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - - var expectedResult = people.OrderBy(p => p, _comparer).Select(p => new KeyValuePair(p.Name, p)).ToList(); - var actualResult = _results.Messages[0].SortedItems.ToList(); - - actualResult.Should().BeEquivalentTo(expectedResult); - } + var disposable = cache.Connect().Sort(new BehaviorSubject>(SortExpressionComparer.Ascending(d => d.Id))).Subscribe(); - [Fact] - public void AppendAtBeginning() - { - var people = _generator.Take(100).ToArray(); - _source.AddOrUpdate(people); - - //create age 0 to ensure it is inserted first - var insert = new Person("_Aaron", 0); - - _source.AddOrUpdate(insert); - - _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("_Aaron"); - - indexedItem.HasValue.Should().BeTrue(); - indexedItem.Value.Index.Should().Be(0, "Inserted item should have index of zero"); + disposable.Dispose(); } [Fact] - public void AppendInMiddle() + public void InlineUpdateProducesAReplace() { - var people = _generator.Take(100).ToArray(); + var people = _generator.Take(10).ToArray(); _source.AddOrUpdate(people); + var toupdate = people[3]; - //create age 0 to ensure it is inserted first - var insert = new Person("Marvin", 50); - - _source.AddOrUpdate(insert); - - _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("Marvin"); + _source.AddOrUpdate(new Person(toupdate.Name, toupdate.Age + 1)); - indexedItem.HasValue.Should().BeTrue(); + var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); + var adaptor = new SortedObservableCollectionAdaptor(); + adaptor.Adapt(_results.Messages.Last(), list); - var list = _results.Messages[1].SortedItems.ToList(); - var sortedResult = list.OrderBy(p => _comparer).ToList(); - list.Should().BeEquivalentTo(sortedResult); + var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); + list.Should().BeEquivalentTo(shouldbe); } [Fact] - public void AppendAtEnd() + public void RemoveFirst() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); //create age 0 to ensure it is inserted first - var insert = new Person("zzzzz", 1000); - - _source.AddOrUpdate(insert); + var remove = _results.Messages[0].SortedItems.First(); - _results.Data.Count.Should().Be(101, "Should be 101 people in the cache"); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup("zzzzz"); + _source.Remove(remove.Key); - indexedItem.HasValue.Should().BeTrue(); + _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + //TODO: fixed Text + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); + indexedItem.HasValue.Should().BeFalse(); var list = _results.Messages[1].SortedItems.ToList(); var sortedResult = list.OrderBy(p => _comparer).ToList(); @@ -783,18 +877,18 @@ public void AppendAtEnd() } [Fact] - public void RemoveFirst() + public void RemoveFromEnd() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); //create age 0 to ensure it is inserted first - var remove = _results.Messages[0].SortedItems.First(); + var remove = _results.Messages[0].SortedItems.Last(); _source.Remove(remove.Key); _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); - //TODO: fixed Text + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); indexedItem.HasValue.Should().BeFalse(); @@ -826,62 +920,75 @@ public void RemoveFromMiddle() } [Fact] - public void RemoveFromEnd() + public void SortAfterFilter() { - var people = _generator.Take(100).ToArray(); - _source.AddOrUpdate(people); + var source = new SourceCache(p => p.Key); - //create age 0 to ensure it is inserted first - var remove = _results.Messages[0].SortedItems.Last(); + var filterSubject = new BehaviorSubject>(p => true); - _source.Remove(remove.Key); + var agg = new SortedChangeSetAggregator(source.Connect().Filter(filterSubject).Group(x => (TestString)x.Key).Transform(x => new ViewModel(x.Key)).Sort(new ViewModel.Comparer())); - _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + source.Edit( + x => + { + x.AddOrUpdate(new Person("A", 1, "F")); + x.AddOrUpdate(new Person("a", 1, "M")); + x.AddOrUpdate(new Person("B", 1, "F")); + x.AddOrUpdate(new Person("b", 1, "M")); + }); - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(remove.Key); - indexedItem.HasValue.Should().BeFalse(); + filterSubject.OnNext(p => p.Name.Equals("a", StringComparison.OrdinalIgnoreCase)); + } - var list = _results.Messages[1].SortedItems.ToList(); - var sortedResult = list.OrderBy(p => _comparer).ToList(); - list.Should().BeEquivalentTo(sortedResult); + [Fact] + public void SortAfterFilterList() + { + var source = new SourceList(); + + var filterSubject = new BehaviorSubject>(p => true); + + var agg = source.Connect().Filter(filterSubject).Transform(x => new ViewModel(x.Name)).Sort(new ViewModel.Comparer()).AsAggregator(); + + source.Edit( + x => + { + x.Add(new Person("A", 1, "F")); + x.Add(new Person("a", 1, "M")); + x.Add(new Person("B", 1, "F")); + x.Add(new Person("b", 1, "M")); + }); + + filterSubject.OnNext(p => p.Name.Equals("a", StringComparison.OrdinalIgnoreCase)); } [Fact] - public void UpdateFirst() + public void SortInitialBatch() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - var toupdate = _results.Messages[0].SortedItems.First().Value; - var update = new Person(toupdate.Name, toupdate.Age + 5); + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - _source.AddOrUpdate(update); + var expectedResult = people.OrderBy(p => p, _comparer).Select(p => new KeyValuePair(p.Name, p)).ToList(); + var actualResult = _results.Messages[0].SortedItems.ToList(); - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - //TODO: fixed Text - var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); - indexedItem.HasValue.Should().BeTrue(); - ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); - var list = _results.Messages[1].SortedItems.ToList(); - var sortedResult = list.OrderBy(p => _comparer).ToList(); - list.Should().BeEquivalentTo(sortedResult); + actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void UpdateMiddle() + public void UpdateFirst() { var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - var toupdate = _results.Messages[0].SortedItems.Skip(50).First().Value; + var toupdate = _results.Messages[0].SortedItems.First().Value; var update = new Person(toupdate.Name, toupdate.Age + 5); _source.AddOrUpdate(update); _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - + //TODO: fixed Text var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); - indexedItem.HasValue.Should().BeTrue(); ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); var list = _results.Messages[1].SortedItems.ToList(); @@ -913,199 +1020,91 @@ public void UpdateLast() } [Fact] - public void BatchUpdate1() + public void UpdateMiddle() { - var people = _generator.Take(10).ToArray(); + var people = _generator.Take(100).ToArray(); _source.AddOrUpdate(people); - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - var toupdate = people[3]; + var toupdate = _results.Messages[0].SortedItems.Skip(50).First().Value; + var update = new Person(toupdate.Name, toupdate.Age + 5); - _source.Edit(updater => - { - updater.Remove(people[0].Key); - updater.Remove(people[1].Key); - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); - updater.Remove(people[7]); - }); + _source.AddOrUpdate(update); - var adaptor = new SortedObservableCollectionAdaptor(); + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - adaptor.Adapt(_results.Messages.Last(), list); + var indexedItem = _results.Messages[1].SortedItems.Indexed().Lookup(update.Key); - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); - list.Should().BeEquivalentTo(shouldbe); + indexedItem.HasValue.Should().BeTrue(); + ReferenceEquals(update, indexedItem.Value.Value).Should().BeTrue(); + var list = _results.Messages[1].SortedItems.ToList(); + var sortedResult = list.OrderBy(p => _comparer).ToList(); + list.Should().BeEquivalentTo(sortedResult); } - [Fact] - public void BatchUpdateWhereUpdateMovesTheIndexDown() + public class Data { - var people = _generator.Take(10).ToArray(); - _source.AddOrUpdate(people); - - var toupdate = people[3]; - - _source.Edit(updater => + public Data(int id, string value) { - updater.Remove(people[0].Key); - updater.Remove(people[1].Key); - - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age + 50)); - - updater.Remove(people[7]); - }); + Id = id; + Value = value; + } - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - var adaptor = new SortedObservableCollectionAdaptor(); - adaptor.Adapt(_results.Messages.Last(), list); + public int Id { get; } - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); - list.Should().BeEquivalentTo(shouldbe); + public string Value { get; } } - [Fact] - public void BatchUpdate2() + public class TestString : IEquatable { - var people = _generator.Take(10).ToArray(); - _source.AddOrUpdate(people); - - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - - var toupdate = people[3]; + private readonly string _name; - _source.Edit(updater => + public TestString(string name) { - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); - updater.AddOrUpdate(new Person("Mr", "Z", 50, "M")); - }); - - var adaptor = new SortedObservableCollectionAdaptor(); - - adaptor.Adapt(_results.Messages.Last(), list); - - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); - list.Should().BeEquivalentTo(shouldbe); - } - - [Fact] - public void BatchUpdate3() - { - var people = _generator.Take(10).ToArray(); - _source.AddOrUpdate(people); - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - - var toupdate = people[7]; + _name = name; + } - _source.Edit(updater => + public static implicit operator TestString(string source) { - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); - updater.AddOrUpdate(new Person("Mr", "A", 10, "M")); - updater.AddOrUpdate(new Person("Mr", "B", 40, "M")); - updater.AddOrUpdate(new Person("Mr", "C", 70, "M")); - }); - - var adaptor = new SortedObservableCollectionAdaptor(); - - adaptor.Adapt(_results.Messages.Last(), list); - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); - list.Should().BeEquivalentTo(shouldbe); - } - - [Fact] - public void BatchUpdateShiftingIndicies() - { - var testData = new[] { - new Person("A", 3), - new Person("B", 5), - new Person("C", 7), - new Person("D", 8), - new Person("E", 10), - new Person("F", 12), - new Person("G", 14) - }; - _source.AddOrUpdate(testData); - var list = new ObservableCollectionExtended(testData.OrderBy(p => p, _comparer)); - - var toUpdate1 = testData[0]; - var toUpdate2 = testData[3]; + return new TestString(source); + } - _source.Edit(updater => + public static implicit operator string(TestString source) { - updater.AddOrUpdate(new Person(toUpdate1.Name, 6)); - updater.AddOrUpdate(new Person(toUpdate2.Name, 2)); - }); - - var adaptor = new SortedObservableCollectionAdaptor(); - - adaptor.Adapt(_results.Messages.Last(), list); - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); - list.Should().BeEquivalentTo(shouldbe); - } - - [Fact] - public void BatchUpdate4() - { - var people = _generator.Take(10).ToArray(); - _source.AddOrUpdate(people); - - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - - var toupdate = people[3]; + return source._name; + } - _source.Edit(updater => + public bool Equals(TestString? other) { - updater.AddOrUpdate(new Person(toupdate.Name, toupdate.Age - 24)); - updater.AddOrUpdate(new Person("Mr", "A", 10, "M")); - updater.Remove(people[5]); - updater.AddOrUpdate(new Person("Mr", "C", 70, "M")); - }); - - var adaptor = new SortedObservableCollectionAdaptor(); - - adaptor.Adapt(_results.Messages.Last(), list); - - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); - list.Should().BeEquivalentTo(shouldbe); - } - - [Fact] - public void BatchUpdate6() - { - var people = _generator.Take(10).ToArray(); - _source.AddOrUpdate(people); + return StringComparer.OrdinalIgnoreCase.Equals(_name, other?._name); + } - _source.Edit(updater => + public override bool Equals(object? obj) { - updater.Clear(); - updater.AddOrUpdate(_generator.Take(10).ToArray()); - updater.Clear(); - }); - - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - - var adaptor = new SortedObservableCollectionAdaptor(); - - adaptor.Adapt(_results.Messages.Last(), list); + return obj is TestString testString && Equals(testString); + } - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); - list.Should().BeEquivalentTo(shouldbe); + public override int GetHashCode() + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(_name); + } } - [Fact] - public void InlineUpdateProducesAReplace() + public class ViewModel { - var people = _generator.Take(10).ToArray(); - _source.AddOrUpdate(people); - var toupdate = people[3]; - - _source.AddOrUpdate(new Person(toupdate.Name, toupdate.Age + 1)); + public ViewModel(string name) + { + this.Name = name; + } - var list = new ObservableCollectionExtended(people.OrderBy(p => p, _comparer)); - var adaptor = new SortedObservableCollectionAdaptor(); - adaptor.Adapt(_results.Messages.Last(), list); + public string Name { get; set; } - var shouldbe = _results.Messages.Last().SortedItems.Select(p => p.Value).ToList(); - list.Should().BeEquivalentTo(shouldbe); + public class Comparer : IComparer + { + public int Compare(ViewModel? x, ViewModel? y) + { + return StringComparer.OrdinalIgnoreCase.Compare(x?.Name, y?.Name); + } + } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/SortObservableFixtureFixture.cs b/src/DynamicData.Tests/Cache/SortObservableFixtureFixture.cs index 2c6ad9522..28fd06f65 100644 --- a/src/DynamicData.Tests/Cache/SortObservableFixtureFixture.cs +++ b/src/DynamicData.Tests/Cache/SortObservableFixtureFixture.cs @@ -2,56 +2,38 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class SortObservableFixture: IDisposable + public class SortObservableFixture : IDisposable { private readonly ISourceCache _cache; - private readonly SortedChangeSetAggregator _results; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); - private readonly BehaviorSubject> _comparerObservable; + private readonly SortExpressionComparer _comparer; + private readonly BehaviorSubject> _comparerObservable; + + private readonly RandomPersonGenerator _generator = new(); + + private readonly SortedChangeSetAggregator _results; + // private IComparer _comparer; - public SortObservableFixture() + public SortObservableFixture() { _comparer = SortExpressionComparer.Ascending(p => p.Name).ThenByAscending(p => p.Age); _comparerObservable = new BehaviorSubject>(_comparer); _cache = new SourceCache(p => p.Name); - // _sortController = new SortController(_comparer); - - _results = new SortedChangeSetAggregator - ( - _cache.Connect().Sort(_comparerObservable, resetThreshold:25) - ); - } + // _sortController = new SortController(_comparer); - public void Dispose() - { - _cache.Dispose(); - _results.Dispose(); - _comparerObservable.OnCompleted(); - } - - [Fact] - public void SortInitialBatch() - { - var people = _generator.Take(100).ToArray(); - _cache.AddOrUpdate(people); - - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - - var expectedResult = people.OrderBy(p => p, _comparer).Select(p => new KeyValuePair(p.Name, p)).ToList(); - var actualResult = _results.Messages[0].SortedItems.ToList(); - - actualResult.Should().BeEquivalentTo(expectedResult); + _results = new SortedChangeSetAggregator(_cache.Connect().Sort(_comparerObservable, resetThreshold: 25)); } [Fact] @@ -71,9 +53,9 @@ public void ChangeSort() } [Fact] - public void ChangeSortWithinThreshold() + public void ChangeSortAboveThreshold() { - var people = _generator.Take(20).ToArray(); + var people = _generator.Take(30).ToArray(); _cache.AddOrUpdate(people); var desc = SortExpressionComparer.Descending(p => p.Age).ThenByAscending(p => p.Name); @@ -84,13 +66,13 @@ public void ChangeSortWithinThreshold() var actualResult = items.ToList(); var sortReason = items.SortReason; actualResult.Should().BeEquivalentTo(expectedResult); - sortReason.Should().Be(SortReason.Reorder); + sortReason.Should().Be(SortReason.Reset); } [Fact] - public void ChangeSortAboveThreshold() + public void ChangeSortWithinThreshold() { - var people = _generator.Take(30).ToArray(); + var people = _generator.Take(20).ToArray(); _cache.AddOrUpdate(people); var desc = SortExpressionComparer.Descending(p => p.Age).ThenByAscending(p => p.Name); @@ -101,7 +83,14 @@ public void ChangeSortAboveThreshold() var actualResult = items.ToList(); var sortReason = items.SortReason; actualResult.Should().BeEquivalentTo(expectedResult); - sortReason.Should().Be(SortReason.Reset); + sortReason.Should().Be(SortReason.Reorder); + } + + public void Dispose() + { + _cache.Dispose(); + _results.Dispose(); + _comparerObservable.OnCompleted(); } [Fact] @@ -144,5 +133,19 @@ public void Reset() var actualResult = _results.Messages[2].SortedItems.ToList(); actualResult.Should().BeEquivalentTo(expectedResult); } + + [Fact] + public void SortInitialBatch() + { + var people = _generator.Take(100).ToArray(); + _cache.AddOrUpdate(people); + + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + + var expectedResult = people.OrderBy(p => p, _comparer).Select(p => new KeyValuePair(p.Name, p)).ToList(); + var actualResult = _results.Messages[0].SortedItems.ToList(); + + actualResult.Should().BeEquivalentTo(expectedResult); + } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/SourceCacheFixture.cs b/src/DynamicData.Tests/Cache/SourceCacheFixture.cs index 3fa8c5412..73f906be9 100644 --- a/src/DynamicData.Tests/Cache/SourceCacheFixture.cs +++ b/src/DynamicData.Tests/Cache/SourceCacheFixture.cs @@ -1,15 +1,18 @@ using System; using System.Reactive.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class SourceCacheFixture : IDisposable { private readonly ChangeSetAggregator _results; + private readonly ISourceCache _source; public SourceCacheFixture() @@ -18,27 +21,22 @@ public SourceCacheFixture() _results = _source.Connect().AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void CanHandleABatchOfUpdates() { - _source.Edit(updater => - { - var torequery = new Person("Adult1", 44); - - updater.AddOrUpdate(new Person("Adult1", 40)); - updater.AddOrUpdate(new Person("Adult1", 41)); - updater.AddOrUpdate(new Person("Adult1", 42)); - updater.AddOrUpdate(new Person("Adult1", 43)); - updater.Refresh(torequery); - updater.Remove(torequery); - updater.Refresh(torequery); - }); + _source.Edit( + updater => + { + var torequery = new Person("Adult1", 44); + + updater.AddOrUpdate(new Person("Adult1", 40)); + updater.AddOrUpdate(new Person("Adult1", 41)); + updater.AddOrUpdate(new Person("Adult1", 42)); + updater.AddOrUpdate(new Person("Adult1", 43)); + updater.Refresh(torequery); + updater.Remove(torequery); + updater.Refresh(torequery); + }); _results.Summary.Overall.Count.Should().Be(6, "Should be 6 up`dates"); _results.Messages.Count.Should().Be(1, "Should be 1 message"); @@ -50,6 +48,31 @@ public void CanHandleABatchOfUpdates() _results.Data.Count.Should().Be(0, "Should be 1 item in` the cache"); } + [Fact] + public void CountChanged() + { + int count = 0; + int invoked = 0; + using (_source.CountChanged.Subscribe( + c => + { + count = c; + invoked++; + })) + { + invoked.Should().Be(1); + count.Should().Be(0); + + _source.AddOrUpdate(new RandomPersonGenerator().Take(100)); + invoked.Should().Be(2); + count.Should().Be(100); + + _source.Clear(); + invoked.Should().Be(3); + count.Should().Be(0); + } + } + [Fact] public void CountChangedShouldAlwaysInvokeUponeSubscription() { @@ -57,6 +80,12 @@ public void CountChangedShouldAlwaysInvokeUponeSubscription() var subscription = _source.CountChanged.Subscribe(count => result = count); result.HasValue.Should().BeTrue(); + + if (result is null) + { + throw new InvalidOperationException(nameof(result)); + } + result.Value.Should().Be(0, "Count should be zero"); subscription.Dispose(); @@ -71,20 +100,29 @@ public void CountChangedShouldReflectContentsOfCacheInvokeUponSubscription() _source.AddOrUpdate(generator.Take(100)); + if (result is null) + { + throw new InvalidOperationException(nameof(result)); + } + result.HasValue.Should().BeTrue(); result.Value.Should().Be(100, "Count should be 100"); subscription.Dispose(); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + [Fact] public void SubscribesDisposesCorrectly() { bool called = false; bool errored = false; bool completed = false; - var subscription = _source.Connect() - .Finally(() => completed = true) - .Subscribe(updates => { called = true; }, ex => errored = true, () => completed = true); + var subscription = _source.Connect().Finally(() => completed = true).Subscribe(updates => { called = true; }, ex => errored = true, () => completed = true); _source.AddOrUpdate(new Person("Adult1", 40)); subscription.Dispose(); @@ -94,29 +132,5 @@ public void SubscribesDisposesCorrectly() called.Should().BeTrue(); completed.Should().BeTrue(); } - - [Fact] - public void CountChanged() - { - int count = 0; - int invoked = 0; - using (_source.CountChanged.Subscribe(c => - { - count = c; - invoked++; - })) - { - invoked.Should().Be(1); - count.Should().Be(0); - - _source.AddOrUpdate(new RandomPersonGenerator().Take(100)); - invoked.Should().Be(2); - count.Should().Be(100); - - _source.Clear(); - invoked.Should().Be(3); - count.Should().Be(0); - } - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/SubscribeManyFixture.cs b/src/DynamicData.Tests/Cache/SubscribeManyFixture.cs index 35c56e1af..02ce2f803 100644 --- a/src/DynamicData.Tests/Cache/SubscribeManyFixture.cs +++ b/src/DynamicData.Tests/Cache/SubscribeManyFixture.cs @@ -1,47 +1,39 @@ using System; using System.Linq; using System.Reactive.Disposables; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class SubscribeManyFixture: IDisposable + public class SubscribeManyFixture : IDisposable { - private class SubscribeableObject - { - public bool IsSubscribed { get; private set; } - public int Id { get; } - - public void Subscribe() - { - IsSubscribed = true; - } - - public void UnSubscribe() - { - IsSubscribed = false; - } - - public SubscribeableObject(int id) - { - Id = id; - } - } + private readonly ChangeSetAggregator _results; private readonly ISourceCache _source; - private readonly ChangeSetAggregator _results; - public SubscribeManyFixture() + public SubscribeManyFixture() { _source = new SourceCache(p => p.Id); _results = new ChangeSetAggregator( - _source.Connect().SubscribeMany(subscribeable => - { - subscribeable.Subscribe(); - return Disposable.Create(subscribeable.UnSubscribe); - })); + _source.Connect().SubscribeMany( + subscribeable => + { + subscribeable.Subscribe(); + return Disposable.Create(subscribeable.UnSubscribe); + })); + } + + [Fact] + public void AddedItemWillbeSubscribed() + { + _source.AddOrUpdate(new SubscribeableObject(1)); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Items.First().IsSubscribed.Should().Be(true, "Should be subscribed"); } public void Dispose() @@ -51,13 +43,13 @@ public void Dispose() } [Fact] - public void AddedItemWillbeSubscribed() + public void EverythingIsUnsubscribedWhenStreamIsDisposed() { - _source.AddOrUpdate(new SubscribeableObject(1)); + _source.AddOrUpdate(Enumerable.Range(1, 10).Select(i => new SubscribeableObject(i))); + _source.Clear(); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - _results.Data.Items.First().IsSubscribed.Should().Be(true, "Should be subscribed"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[1].All(d => !d.Current.IsSubscribed).Should().BeTrue(); } [Fact] @@ -83,14 +75,26 @@ public void UpdateUnsubscribesPrevious() _results.Messages[1].First().Previous.Value.IsSubscribed.Should().Be(false, "Previous should not be subscribed"); } - [Fact] - public void EverythingIsUnsubscribedWhenStreamIsDisposed() + private class SubscribeableObject { - _source.AddOrUpdate(Enumerable.Range(1, 10).Select(i => new SubscribeableObject(i))); - _source.Clear(); + public SubscribeableObject(int id) + { + Id = id; + } - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[1].All(d => !d.Current.IsSubscribed).Should().BeTrue(); + public int Id { get; } + + public bool IsSubscribed { get; private set; } + + public void Subscribe() + { + IsSubscribed = true; + } + + public void UnSubscribe() + { + IsSubscribed = false; + } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/SwitchFixture.cs b/src/DynamicData.Tests/Cache/SwitchFixture.cs index 6afb6484b..c483b6d5f 100644 --- a/src/DynamicData.Tests/Cache/SwitchFixture.cs +++ b/src/DynamicData.Tests/Cache/SwitchFixture.cs @@ -1,41 +1,30 @@ using System; using System.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class SwitchFixture: IDisposable + public class SwitchFixture : IDisposable { - private readonly ISubject> _switchable; - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; - public SwitchFixture() + private readonly ISourceCache _source; + + private readonly ISubject> _switchable; + + public SwitchFixture() { _source = new SourceCache(p => p.Name); _switchable = new BehaviorSubject>(_source); _results = _switchable.Switch().AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - - [Fact] - public void PoulatesFirstSource() - { - var inital = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); - _source.AddOrUpdate(inital); - - _results.Data.Count.Should().Be(100); - } - [Fact] public void ClearsForNewSource() { @@ -55,8 +44,21 @@ public void ClearsForNewSource() var nextUpdates = Enumerable.Range(101, 100).Select(i => new Person("Person" + i, i)).ToArray(); newSource.AddOrUpdate(nextUpdates); _results.Data.Count.Should().Be(200); + } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); } + [Fact] + public void PoulatesFirstSource() + { + var inital = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); + _source.AddOrUpdate(inital); + + _results.Data.Count.Should().Be(100); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TimeExpiryFixture.cs b/src/DynamicData.Tests/Cache/TimeExpiryFixture.cs index 36e00a159..908b92ae6 100644 --- a/src/DynamicData.Tests/Cache/TimeExpiryFixture.cs +++ b/src/DynamicData.Tests/Cache/TimeExpiryFixture.cs @@ -1,20 +1,27 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { - public class TimeExpiryFixture: IDisposable + public class TimeExpiryFixture : IDisposable { private readonly ISourceCache _cache; + private readonly IDisposable _remover; + private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; - public TimeExpiryFixture() + public TimeExpiryFixture() { _scheduler = new TestScheduler(); @@ -23,13 +30,6 @@ public TimeExpiryFixture() _remover = _cache.ExpireAfter(p => TimeSpan.FromMilliseconds(100), _scheduler).Subscribe(); } - public void Dispose() - { - _results.Dispose(); - _remover.Dispose(); - _cache.Dispose(); - } - [Fact] public void AutoRemove() { @@ -61,25 +61,36 @@ public void AutoRemove() } [Fact] - public void ItemAddedIsExpired() + public void CanHandleABatchOfUpdates() { - _cache.AddOrUpdate(new Person("Name1", 10)); + const int size = 100; + Person[] items = Enumerable.Range(1, size).Select(i => new Person($"Name.{i}", i)).ToArray(); + _cache.AddOrUpdate(items); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(150).Ticks); + _results.Data.Count.Should().Be(0, "Should be no data in the cache"); _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds in the first update"); - _results.Messages[1].Removes.Should().Be(1, "Should be 1 removes in the second update"); + _results.Messages[0].Adds.Should().Be(100, "Should be 100 adds in the first message"); + _results.Messages[1].Removes.Should().Be(100, "Should be 100 removes in the second message"); + } + + public void Dispose() + { + _results.Dispose(); + _remover.Dispose(); + _cache.Dispose(); } [Fact] public void ExpireIsCancelledWhenUpdated() { - _cache.Edit(updater => - { - updater.AddOrUpdate(new Person("Name1", 20)); - updater.AddOrUpdate(new Person("Name1", 21)); - }); + _cache.Edit( + updater => + { + updater.AddOrUpdate(new Person("Name1", 20)); + updater.AddOrUpdate(new Person("Name1", 21)); + }); _scheduler.AdvanceBy(TimeSpan.FromSeconds(150).Ticks); @@ -91,18 +102,15 @@ public void ExpireIsCancelledWhenUpdated() } [Fact] - public void CanHandleABatchOfUpdates() + public void ItemAddedIsExpired() { - const int size = 100; - Person[] items = Enumerable.Range(1, size).Select(i => new Person($"Name.{i}", i)).ToArray(); + _cache.AddOrUpdate(new Person("Name1", 10)); - _cache.AddOrUpdate(items); _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(150).Ticks); - _results.Data.Count.Should().Be(0, "Should be no data in the cache"); _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should be 100 adds in the first message"); - _results.Messages[1].Removes.Should().Be(100, "Should be 100 removes in the second message"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds in the first update"); + _results.Messages[1].Removes.Should().Be(1, "Should be 1 removes in the second update"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ToObservableChangeSetFixture.cs b/src/DynamicData.Tests/Cache/ToObservableChangeSetFixture.cs index 2f90cfbd2..feab44118 100644 --- a/src/DynamicData.Tests/Cache/ToObservableChangeSetFixture.cs +++ b/src/DynamicData.Tests/Cache/ToObservableChangeSetFixture.cs @@ -1,38 +1,45 @@ using System; using System.Collections.Generic; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache { - public class ToObservableChangeSetFixture : ReactiveTest, IDisposable { + private readonly IDisposable _disposable; + private readonly IObservable _observable; + + private readonly Person _person1 = new("One", 1); + + private readonly Person _person2 = new("Two", 2); + + private readonly Person _person3 = new("Three", 3); + private readonly TestScheduler _scheduler; - private readonly IDisposable _disposable; - private readonly List _target; - private readonly Person _person1 = new Person("One", 1); - private readonly Person _person2 = new Person("Two", 2); - private readonly Person _person3 = new Person("Three", 3); + private readonly List _target; public ToObservableChangeSetFixture() { _scheduler = new TestScheduler(); - _observable = _scheduler.CreateColdObservable( - OnNext(1, _person1), - OnNext(2, _person2), - OnNext(3, _person3)); + _observable = _scheduler.CreateColdObservable(OnNext(1, _person1), OnNext(2, _person2), OnNext(3, _person3)); _target = new List(); - _disposable = _observable - .ToObservableChangeSet(p => p.Key, limitSizeTo: 2, scheduler: _scheduler) - .Clone(_target) - .Subscribe(); + _disposable = _observable.ToObservableChangeSet(p => p.Key, limitSizeTo: 2, scheduler: _scheduler).Clone(_target).Subscribe(); + } + + public void Dispose() + { + _disposable.Dispose(); } [Fact] @@ -46,11 +53,5 @@ public void ShouldLimitSizeOfBoundCollection() _target.Count.Should().Be(2, "Should be 2 item in target collection because of size limit"); } - - public void Dispose() - { - _disposable.Dispose(); - } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ToObservableChangeSetFixtureWithCompletion.cs b/src/DynamicData.Tests/Cache/ToObservableChangeSetFixtureWithCompletion.cs index c49023bc1..5b9e1c183 100644 --- a/src/DynamicData.Tests/Cache/ToObservableChangeSetFixtureWithCompletion.cs +++ b/src/DynamicData.Tests/Cache/ToObservableChangeSetFixtureWithCompletion.cs @@ -1,17 +1,23 @@ using System; using System.Collections.Generic; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { public class ToObservableChangeSetFixtureWithCompletion : IDisposable { - private readonly ISubject _observable; private readonly IDisposable _disposable; + + private readonly ISubject _observable; + private readonly List _target; + private bool _hasCompleted = false; public ToObservableChangeSetFixtureWithCompletion() @@ -20,10 +26,12 @@ public ToObservableChangeSetFixtureWithCompletion() _target = new List(); - _disposable = _observable - .ToObservableChangeSet(p => p.Key) - .Clone(_target) - .Subscribe(x => { }, () => _hasCompleted = true); + _disposable = _observable.ToObservableChangeSet(p => p.Key).Clone(_target).Subscribe(x => { }, () => _hasCompleted = true); + } + + public void Dispose() + { + _disposable.Dispose(); } [Fact] @@ -39,13 +47,6 @@ public void ShouldReceiveUpdatesThenComplete() _observable.OnNext(new Person("Three", 3)); _target.Count.Should().Be(2); - - } - - public void Dispose() - { - _disposable.Dispose(); } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/ToSortedCollectionFixture.cs b/src/DynamicData.Tests/Cache/ToSortedCollectionFixture.cs index 7ad684376..178809ebc 100644 --- a/src/DynamicData.Tests/Cache/ToSortedCollectionFixture.cs +++ b/src/DynamicData.Tests/Cache/ToSortedCollectionFixture.cs @@ -3,10 +3,14 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache @@ -14,9 +18,12 @@ namespace DynamicData.Tests.Cache public class ToSortedCollectionFixture : IDisposable { private readonly SourceCache _cache; - private readonly List _sortedCollection = new List(); - private readonly List _unsortedCollection = new List(); - private readonly CompositeDisposable _cleanup = new CompositeDisposable(); + + private readonly CompositeDisposable _cleanup = new(); + + private readonly List _sortedCollection = new(); + + private readonly List _unsortedCollection = new(); public ToSortedCollectionFixture() { @@ -33,28 +40,23 @@ public void Dispose() [Fact] public void SortAscending() { - TestScheduler testScheduler = new TestScheduler(); - - _cleanup.Add(_cache.Connect() - .ObserveOn(testScheduler) - .Sort(SortExpressionComparer.Ascending(p => p.Age)) - .ToCollection() - .Do(persons => - { - _unsortedCollection.Clear(); - _unsortedCollection.AddRange(persons); - }) - .Subscribe()); - - _cleanup.Add(_cache.Connect() - .ObserveOn(testScheduler) - .ToSortedCollection(p => p.Age) - .Do(persons => - { - _sortedCollection.Clear(); - _sortedCollection.AddRange(persons); - }) - .Subscribe()); + TestScheduler testScheduler = new(); + + _cleanup.Add( + _cache.Connect().ObserveOn(testScheduler).Sort(SortExpressionComparer.Ascending(p => p.Age)).ToCollection().Do( + persons => + { + _unsortedCollection.Clear(); + _unsortedCollection.AddRange(persons); + }).Subscribe()); + + _cleanup.Add( + _cache.Connect().ObserveOn(testScheduler).ToSortedCollection(p => p.Age).Do( + persons => + { + _sortedCollection.Clear(); + _sortedCollection.AddRange(persons); + }).Subscribe()); // Insert an item with a lower sort order _cache.AddOrUpdate(new Person("Name", 0)); @@ -69,28 +71,23 @@ public void SortAscending() [Fact] public void SortDescending() { - TestScheduler testScheduler = new TestScheduler(); - - _cleanup.Add(_cache.Connect() - .ObserveOn(testScheduler) - .Sort(SortExpressionComparer.Ascending(p => p.Age)) - .ToCollection() - .Do(persons => - { - _unsortedCollection.Clear(); - _unsortedCollection.AddRange(persons); - }) - .Subscribe()); - - _cleanup.Add(_cache.Connect() - .ObserveOn(testScheduler) - .ToSortedCollection(p => p.Age, SortDirection.Descending) - .Do(persons => - { - _sortedCollection.Clear(); - _sortedCollection.AddRange(persons); - }) - .Subscribe()); + TestScheduler testScheduler = new(); + + _cleanup.Add( + _cache.Connect().ObserveOn(testScheduler).Sort(SortExpressionComparer.Ascending(p => p.Age)).ToCollection().Do( + persons => + { + _unsortedCollection.Clear(); + _unsortedCollection.AddRange(persons); + }).Subscribe()); + + _cleanup.Add( + _cache.Connect().ObserveOn(testScheduler).ToSortedCollection(p => p.Age, SortDirection.Descending).Do( + persons => + { + _sortedCollection.Clear(); + _sortedCollection.AddRange(persons); + }).Subscribe()); // Insert an item with a lower sort order _cache.AddOrUpdate(new Person("Name", 0)); diff --git a/src/DynamicData.Tests/Cache/TransformAsyncFixture.cs b/src/DynamicData.Tests/Cache/TransformAsyncFixture.cs index 03a4fca43..b873d2c7a 100644 --- a/src/DynamicData.Tests/Cache/TransformAsyncFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformAsyncFixture.cs @@ -4,8 +4,11 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache @@ -13,69 +16,59 @@ namespace DynamicData.Tests.Cache public class TransformAsyncFixture { [Fact] - public void ReTransformAll() + public async Task Add() { - var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); - var forceTransform = new Subject(); + using var stub = new TransformStub(); + var person = new Person("Adult1", 50); + stub.Source.AddOrUpdate(person); - using (var stub = new TransformStub(forceTransform)) - { - stub.Source.AddOrUpdate(people); - forceTransform.OnNext(Unit.Default); + stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); + stub.Results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - stub.Results.Messages.Count.Should().Be(2); - stub.Results.Messages[1].Updates.Should().Be(10); + var firstPerson = await stub.TransformFactory(person); - for (int i = 1; i <= 10; i++) - { - var original = stub.Results.Messages[0].ElementAt(i - 1).Current; - var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; - - updated.Should().Be(original); - ReferenceEquals(original, updated).Should().BeFalse(); - } - } + stub.Results.Data.Items.First().Should().Be(firstPerson, "Should be same person"); } [Fact] - public void ReTransformSelected() + public async Task BatchOfUniqueUpdates() { - var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); - var forceTransform = new Subject>(); + var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); + using var stub = new TransformStub(); + stub.Source.AddOrUpdate(people); - using (var stub = new TransformStub(forceTransform)) - { - stub.Source.AddOrUpdate(people); - forceTransform.OnNext(person => person.Age <= 5); - - stub.Results.Messages.Count.Should().Be(2); - stub.Results.Messages[1].Updates.Should().Be(5); - - for (int i = 1; i <= 5; i++) - { - var original = stub.Results.Messages[0].ElementAt(i - 1).Current; - var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; - updated.Should().Be(original); - ReferenceEquals(original, updated).Should().BeFalse(); - } - } + // Thread.Sleep(10000); + + stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); + stub.Results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); + + var result = await Task.WhenAll(people.Select(stub.TransformFactory)); + var transformed = result.OrderBy(p => p.Age).ToArray(); + stub.Results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(stub.Results.Data.Items.OrderBy(p => p.Age), "Incorrect transform result"); } [Fact] - public async Task Add() + public void Clear() { - using (var stub = new TransformStub()) - { - var person = new Person("Adult1", 50); - stub.Source.AddOrUpdate(person); + using var stub = new TransformStub(); + var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); - stub.Results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + stub.Source.AddOrUpdate(people); + stub.Source.Clear(); - var firstPerson = await stub.TransformFactory(person); + stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); + stub.Results.Messages[0].Adds.Should().Be(100, "Should be 80 adds"); + stub.Results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); + stub.Results.Data.Count.Should().Be(0, "Should be nothing cached"); + } - stub.Results.Data.Items.First().Should().Be(firstPerson, "Should be same person"); - } + [Fact] + public void HandleError() + { + using var stub = new TransformStub(p => throw new Exception("Broken")); + stub.Source.AddOrUpdate(new Person("Name1", 1)); + + stub.Results.Error.Should().NotBeNull(); } [Fact] @@ -84,179 +77,165 @@ public void Remove() const string key = "Adult1"; var person = new Person(key, 50); - using (var stub = new TransformStub()) - { - stub.Source.AddOrUpdate(person); - stub.Source.Remove(key); - - stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); - stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); - stub.Results.Messages[0].Adds.Should().Be(1, "Should be 80 addes"); - stub.Results.Messages[1].Removes.Should().Be(1, "Should be 80 removes"); - stub.Results.Data.Count.Should().Be(0, "Should be nothing cached"); - } + using var stub = new TransformStub(); + stub.Source.AddOrUpdate(person); + stub.Source.Remove(key); + + stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); + stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); + stub.Results.Messages[0].Adds.Should().Be(1, "Should be 80 addes"); + stub.Results.Messages[1].Removes.Should().Be(1, "Should be 80 removes"); + stub.Results.Data.Count.Should().Be(0, "Should be nothing cached"); } [Fact] - public void Update() + public void ReTransformAll() { - const string key = "Adult1"; - var newperson = new Person(key, 50); - var updated = new Person(key, 51); + var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); + var forceTransform = new Subject(); + + using var stub = new TransformStub(forceTransform); + stub.Source.AddOrUpdate(people); + forceTransform.OnNext(Unit.Default); + + stub.Results.Messages.Count.Should().Be(2); + stub.Results.Messages[1].Updates.Should().Be(10); - using (var stub = new TransformStub()) + for (int i = 1; i <= 10; i++) { - stub.Source.AddOrUpdate(newperson); - stub.Source.AddOrUpdate(updated); + var original = stub.Results.Messages[0].ElementAt(i - 1).Current; + var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; - stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); - stub.Results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - stub.Results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); + updated.Should().Be(original); + ReferenceEquals(original, updated).Should().BeFalse(); } } [Fact] - public async Task BatchOfUniqueUpdates() + public void ReTransformSelected() { - var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new TransformStub()) - { - stub.Source.AddOrUpdate(people); + var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); + var forceTransform = new Subject>(); - // Thread.Sleep(10000); + using var stub = new TransformStub(forceTransform); + stub.Source.AddOrUpdate(people); + forceTransform.OnNext(person => person.Age <= 5); - stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); - stub.Results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); + stub.Results.Messages.Count.Should().Be(2); + stub.Results.Messages[1].Updates.Should().Be(5); - var result = await Task.WhenAll(people.Select(stub.TransformFactory)); - var transformed = result.OrderBy(p => p.Age).ToArray(); - stub.Results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(stub.Results.Data.Items.OrderBy(p => p.Age), "Incorrect transform result"); + for (int i = 1; i <= 5; i++) + { + var original = stub.Results.Messages[0].ElementAt(i - 1).Current; + var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; + updated.Should().Be(original); + ReferenceEquals(original, updated).Should().BeFalse(); } - - } [Fact] public async Task SameKeyChanges() { - using (var stub = new TransformStub()) - { - var people = Enumerable.Range(1, 10).Select(i => new Person("Name", i)).ToArray(); + using var stub = new TransformStub(); + var people = Enumerable.Range(1, 10).Select(i => new Person("Name", i)).ToArray(); - stub.Source.AddOrUpdate(people); + stub.Source.AddOrUpdate(people); - stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); - stub.Results.Messages[0].Adds.Should().Be(1, "Should return 1 adds"); - stub.Results.Messages[0].Updates.Should().Be(9, "Should return 9 adds"); - stub.Results.Data.Count.Should().Be(1, "Should result in 1 record"); + stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); + stub.Results.Messages[0].Adds.Should().Be(1, "Should return 1 adds"); + stub.Results.Messages[0].Updates.Should().Be(9, "Should return 9 adds"); + stub.Results.Data.Count.Should().Be(1, "Should result in 1 record"); - var lastTransformed = await stub.TransformFactory(people.Last()); - var onlyItemInCache = stub.Results.Data.Items.First(); + var lastTransformed = await stub.TransformFactory(people.Last()); + var onlyItemInCache = stub.Results.Data.Items.First(); - onlyItemInCache.Should().Be(lastTransformed, "Incorrect transform result"); - } + onlyItemInCache.Should().Be(lastTransformed, "Incorrect transform result"); } [Fact] - public void Clear() + public void Update() { - using (var stub = new TransformStub()) - { - var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - - stub.Source.AddOrUpdate(people); - stub.Source.Clear(); - - stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); - stub.Results.Messages[0].Adds.Should().Be(100, "Should be 80 adds"); - stub.Results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); - stub.Results.Data.Count.Should().Be(0, "Should be nothing cached"); - } - } + const string key = "Adult1"; + var newperson = new Person(key, 50); + var updated = new Person(key, 51); - [Fact] - public void HandleError() - { - using (var stub = new TransformStub(p => throw new Exception("Broken"))) - { - stub.Source.AddOrUpdate(new Person("Name1" ,1)); + using var stub = new TransformStub(); + stub.Source.AddOrUpdate(newperson); + stub.Source.AddOrUpdate(updated); - stub.Results.Error.Should().NotBeNull(); - - } + stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); + stub.Results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); + stub.Results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); } private class TransformStub : IDisposable { - public ISourceCache Source { get; } = new SourceCache(p => p.Name); - public ChangeSetAggregator Results { get; } - - public Func> TransformFactory { get; } - public TransformStub() { TransformFactory = (p) => - { - var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); - return Task.FromResult(result); - }; - - Results = new ChangeSetAggregator - ( - Source.Connect().TransformAsync(TransformFactory) - ); + { + var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); + return Task.FromResult(result); + }; + + Results = new ChangeSetAggregator(Source.Connect().TransformAsync(TransformFactory)); } public TransformStub(Func factory) { TransformFactory = (p) => - { - var result = factory(p); - return Task.FromResult(result); - }; - - Results = new ChangeSetAggregator - ( - Source.Connect().TransformAsync(TransformFactory) - ); + { + var result = factory(p); + return Task.FromResult(result); + }; + + Results = new ChangeSetAggregator(Source.Connect().TransformAsync(TransformFactory)); } public TransformStub(IObservable retransformer) { TransformFactory = (p) => - { - var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); - return Task.FromResult(result); - }; - - Results = new ChangeSetAggregator - ( - Source.Connect().TransformAsync(TransformFactory, retransformer.Select(x => { - Func transformer = (p, key) => true; - return transformer; - })) - ); + var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); + return Task.FromResult(result); + }; + + Results = new ChangeSetAggregator( + Source.Connect().TransformAsync( + TransformFactory, + retransformer.Select( + x => + { + Func transformer = (p, key) => true; + return transformer; + }))); } public TransformStub(IObservable> retransformer) { TransformFactory = (p) => - { - var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); - return Task.FromResult(result); - }; - - Results = new ChangeSetAggregator - ( - Source.Connect().TransformAsync(TransformFactory, retransformer.Select(selector => { - Func transformed = (p, key) => selector(p); - return transformed; - })) - ); + var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); + return Task.FromResult(result); + }; + + Results = new ChangeSetAggregator( + Source.Connect().TransformAsync( + TransformFactory, + retransformer.Select( + selector => + { + Func transformed = (p, key) => selector(p); + return transformed; + }))); } + public ChangeSetAggregator Results { get; } + + public ISourceCache Source { get; } = new SourceCache(p => p.Name); + + public Func> TransformFactory { get; } + public void Dispose() { Source.Dispose(); diff --git a/src/DynamicData.Tests/Cache/TransformFixture.cs b/src/DynamicData.Tests/Cache/TransformFixture.cs index 46378796e..c314ff874 100644 --- a/src/DynamicData.Tests/Cache/TransformFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformFixture.cs @@ -2,76 +2,56 @@ using System.Linq; using System.Reactive; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class TransformFixture { [Fact] - public void ReTransformAll() + public void Add() { - var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); - var forceTransform = new Subject(); - - using (var stub = new TransformStub(forceTransform)) - { - stub.Source.AddOrUpdate(people); - forceTransform.OnNext(Unit.Default); - - stub.Results.Messages.Count.Should().Be(2); - stub.Results.Messages[1].Updates.Should().Be(10); - - for (int i = 1; i <= 10; i++) - { - var original = stub.Results.Messages[0].ElementAt(i - 1).Current; - var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; + using var stub = new TransformStub(); + var person = new Person("Adult1", 50); + stub.Source.AddOrUpdate(person); - updated.Should().Be(original); - ReferenceEquals(original, updated).Should().BeFalse(); - } - } + stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); + stub.Results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + stub.Results.Data.Items.First().Should().Be(stub.TransformFactory(person), "Should be same person"); } [Fact] - public void ReTransformSelected() + public void BatchOfUniqueUpdates() { - var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); - var forceTransform = new Subject>(); + var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); + using var stub = new TransformStub(); + stub.Source.AddOrUpdate(people); - using (var stub = new TransformStub(forceTransform)) - { - stub.Source.AddOrUpdate(people); - forceTransform.OnNext(person => person.Age <= 5); - - stub.Results.Messages.Count.Should().Be(2); - stub.Results.Messages[1].Updates.Should().Be(5); - - for (int i = 1; i <= 5; i++) - { - var original = stub.Results.Messages[0].ElementAt(i - 1).Current; - var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; - updated.Should().Be(original); - ReferenceEquals(original, updated).Should().BeFalse(); - } - } + stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); + stub.Results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); + + var transformed = people.Select(stub.TransformFactory).OrderBy(p => p.Age).ToArray(); + stub.Results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(transformed, "Incorrect transform result"); } [Fact] - public void Add() + public void Clear() { - using (var stub = new TransformStub()) - { - var person = new Person("Adult1", 50); - stub.Source.AddOrUpdate(person); + using var stub = new TransformStub(); + var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); - stub.Results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - stub.Results.Data.Items.First().Should().Be(stub.TransformFactory(person), "Should be same person"); - } + stub.Source.AddOrUpdate(people); + stub.Source.Clear(); + + stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); + stub.Results.Messages[0].Adds.Should().Be(100, "Should be 80 addes"); + stub.Results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); + stub.Results.Data.Count.Should().Be(0, "Should be nothing cached"); } [Fact] @@ -80,139 +60,132 @@ public void Remove() const string key = "Adult1"; var person = new Person(key, 50); - using (var stub = new TransformStub()) - { - stub.Source.AddOrUpdate(person); - stub.Source.Remove(key); - - stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); - stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); - stub.Results.Messages[0].Adds.Should().Be(1, "Should be 80 addes"); - stub.Results.Messages[1].Removes.Should().Be(1, "Should be 80 removes"); - stub.Results.Data.Count.Should().Be(0, "Should be nothing cached"); - } + using var stub = new TransformStub(); + stub.Source.AddOrUpdate(person); + stub.Source.Remove(key); + + stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); + stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); + stub.Results.Messages[0].Adds.Should().Be(1, "Should be 80 addes"); + stub.Results.Messages[1].Removes.Should().Be(1, "Should be 80 removes"); + stub.Results.Data.Count.Should().Be(0, "Should be nothing cached"); } [Fact] - public void Update() + public void ReTransformAll() { - const string key = "Adult1"; - var newperson = new Person(key, 50); - var updated = new Person(key, 51); + var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); + var forceTransform = new Subject(); - using (var stub = new TransformStub()) + using var stub = new TransformStub(forceTransform); + stub.Source.AddOrUpdate(people); + forceTransform.OnNext(Unit.Default); + + stub.Results.Messages.Count.Should().Be(2); + stub.Results.Messages[1].Updates.Should().Be(10); + + for (int i = 1; i <= 10; i++) { - stub.Source.AddOrUpdate(newperson); - stub.Source.AddOrUpdate(updated); + var original = stub.Results.Messages[0].ElementAt(i - 1).Current; + var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; - stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); - stub.Results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - stub.Results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); + updated.Should().Be(original); + ReferenceEquals(original, updated).Should().BeFalse(); } } [Fact] - public void BatchOfUniqueUpdates() + public void ReTransformSelected() { - var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new TransformStub()) - { - stub.Source.AddOrUpdate(people); + var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); + var forceTransform = new Subject>(); + + using var stub = new TransformStub(forceTransform); + stub.Source.AddOrUpdate(people); + forceTransform.OnNext(person => person.Age <= 5); - stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); - stub.Results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); + stub.Results.Messages.Count.Should().Be(2); + stub.Results.Messages[1].Updates.Should().Be(5); - var transformed = people.Select(stub.TransformFactory).OrderBy(p => p.Age).ToArray(); - stub.Results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(transformed, "Incorrect transform result"); + for (int i = 1; i <= 5; i++) + { + var original = stub.Results.Messages[0].ElementAt(i - 1).Current; + var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; + updated.Should().Be(original); + ReferenceEquals(original, updated).Should().BeFalse(); } } [Fact] public void SameKeyChanges() { - using (var stub = new TransformStub()) - { - var people = Enumerable.Range(1, 10).Select(i => new Person("Name", i)).ToArray(); + using var stub = new TransformStub(); + var people = Enumerable.Range(1, 10).Select(i => new Person("Name", i)).ToArray(); - stub.Source.AddOrUpdate(people); + stub.Source.AddOrUpdate(people); - stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); - stub.Results.Messages[0].Adds.Should().Be(1, "Should return 1 adds"); - stub.Results.Messages[0].Updates.Should().Be(9, "Should return 9 adds"); - stub.Results.Data.Count.Should().Be(1, "Should result in 1 record"); + stub.Results.Messages.Count.Should().Be(1, "Should be 1 updates"); + stub.Results.Messages[0].Adds.Should().Be(1, "Should return 1 adds"); + stub.Results.Messages[0].Updates.Should().Be(9, "Should return 9 adds"); + stub.Results.Data.Count.Should().Be(1, "Should result in 1 record"); - var lastTransformed = stub.TransformFactory(people.Last()); - var onlyItemInCache = stub.Results.Data.Items.First(); + var lastTransformed = stub.TransformFactory(people.Last()); + var onlyItemInCache = stub.Results.Data.Items.First(); - onlyItemInCache.Should().Be(lastTransformed, "Incorrect transform result"); - } + onlyItemInCache.Should().Be(lastTransformed, "Incorrect transform result"); } [Fact] - public void Clear() + public void TransformToNull() { - using (var stub = new TransformStub()) - { - var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - - stub.Source.AddOrUpdate(people); - stub.Source.Clear(); + using var source = new SourceCache(p => p.Name); + using var results = new ChangeSetAggregator(source.Connect().Transform((Func)(p => null))); + source.AddOrUpdate(new Person("Adult1", 50)); - stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); - stub.Results.Messages[0].Adds.Should().Be(100, "Should be 80 addes"); - stub.Results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); - stub.Results.Data.Count.Should().Be(0, "Should be nothing cached"); - } + results.Messages.Count.Should().Be(1, "Should be 1 updates"); + results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + results.Data.Items.First().Should().Be(null, "Should be same person"); } [Fact] - public void TransformToNull() + public void Update() { - using (var source = new SourceCache(p => p.Name)) - using (var results = new ChangeSetAggregator - ( - source.Connect().Transform((Func) (p => null)) - )) - { - source.AddOrUpdate(new Person("Adult1", 50)); + const string key = "Adult1"; + var newperson = new Person(key, 50); + var updated = new Person(key, 51); - results.Messages.Count.Should().Be(1, "Should be 1 updates"); - results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - results.Data.Items.First().Should().Be(null, "Should be same person"); - } + using var stub = new TransformStub(); + stub.Source.AddOrUpdate(newperson); + stub.Source.AddOrUpdate(updated); + + stub.Results.Messages.Count.Should().Be(2, "Should be 2 updates"); + stub.Results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); + stub.Results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); } private class TransformStub : IDisposable { - public ISourceCache Source { get; } = new SourceCache(p => p.Name); - public ChangeSetAggregator Results { get; } - - public Func TransformFactory { get; } = p => new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); - public TransformStub() { - Results = new ChangeSetAggregator - ( - Source.Connect().Transform(TransformFactory) - ); + Results = new ChangeSetAggregator(Source.Connect().Transform(TransformFactory)); } public TransformStub(IObservable retransformer) { - Results = new ChangeSetAggregator - ( - Source.Connect().Transform(TransformFactory, retransformer) - ); + Results = new ChangeSetAggregator(Source.Connect().Transform(TransformFactory, retransformer)); } public TransformStub(IObservable> retransformer) { - Results = new ChangeSetAggregator - ( - Source.Connect().Transform(TransformFactory, retransformer) - ); + Results = new ChangeSetAggregator(Source.Connect().Transform(TransformFactory, retransformer)); } + public ChangeSetAggregator Results { get; } + + public ISourceCache Source { get; } = new SourceCache(p => p.Name); + + public Func TransformFactory { get; } = p => new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); + public void Dispose() { Source.Dispose(); @@ -220,4 +193,4 @@ public void Dispose() } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformFixtureParallel.cs b/src/DynamicData.Tests/Cache/TransformFixtureParallel.cs index eeb1ba292..3b178178e 100644 --- a/src/DynamicData.Tests/Cache/TransformFixtureParallel.cs +++ b/src/DynamicData.Tests/Cache/TransformFixtureParallel.cs @@ -1,25 +1,28 @@ using System; using System.Linq; -using DynamicData.Tests.Domain; + using DynamicData.PLinq; +using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class TransformFixtureParallel : IDisposable { - private ISourceCache _source; - private ChangeSetAggregator _results; - private readonly Func _transformFactory = p => - { - var gender = p.Age % 2 == 0 ? "M" : "F"; - return new PersonWithGender(p, gender); - }; + { + var gender = p.Age % 2 == 0 ? "M" : "F"; + return new PersonWithGender(p, gender); + }; - public TransformFixtureParallel() + private readonly ChangeSetAggregator _results; + + private readonly ISourceCache _source; + + public TransformFixtureParallel() { _source = new SourceCache(p => p.Name); @@ -27,12 +30,6 @@ public TransformFixtureParallel() _results = new ChangeSetAggregator(pTransform); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void Add() { @@ -45,48 +42,54 @@ public void Add() } [Fact] - public void Remove() + public void BatchOfUniqueUpdates() { - const string key = "Adult1"; - var person = new Person(key, 50); + var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); + _source.AddOrUpdate(people); - _source.AddOrUpdate(person); - _source.Remove(key); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 80 addes"); - _results.Messages[1].Removes.Should().Be(1, "Should be 80 removes"); - _results.Data.Count.Should().Be(0, "Should be nothing cached"); + var transformed = people.Select(_transformFactory).ToArray(); + + _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(transformed, "Incorrect transform result"); } [Fact] - public void Update() + public void Clear() { - const string key = "Adult1"; - var newperson = new Person(key, 50); - var updated = new Person(key, 51); + var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - _source.AddOrUpdate(newperson); - _source.AddOrUpdate(updated); + _source.AddOrUpdate(people); + + _source.Clear(); _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - _results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); + _results.Messages[0].Adds.Should().Be(100, "Should be 80 addes"); + _results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); + _results.Data.Count.Should().Be(0, "Should be nothing cached"); } - [Fact] - public void BatchOfUniqueUpdates() + public void Dispose() { - var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - _source.AddOrUpdate(people); + _source.Dispose(); + _results.Dispose(); + } - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); + [Fact] + public void Remove() + { + const string key = "Adult1"; + var person = new Person(key, 50); - var transformed = people.Select(_transformFactory).ToArray(); + _source.AddOrUpdate(person); + _source.Remove(key); - _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(transformed, "Incorrect transform result"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(1, "Should be 80 addes"); + _results.Messages[1].Removes.Should().Be(1, "Should be 80 removes"); + _results.Data.Count.Should().Be(0, "Should be nothing cached"); } [Fact] @@ -107,34 +110,31 @@ public void SameKeyChanges() } [Fact] - public void Clear() + public void TransformToNull() { - var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - - _source.AddOrUpdate(people); - - _source.Clear(); - - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should be 80 addes"); - _results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); - _results.Data.Count.Should().Be(0, "Should be nothing cached"); + using var source = new SourceCache(p => p.Name); + using var results = new ChangeSetAggregator(source.Connect() + .Transform((Func)(p => null), new ParallelisationOptions(ParallelType.Parallelise))); + source.AddOrUpdate(new Person("Adult1", 50)); + + results.Messages.Count.Should().Be(1, "Should be 1 updates"); + results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + results.Data.Items.First().Should().Be(null, "Should be same person"); } [Fact] - public void TransformToNull() + public void Update() { - using (var source = new SourceCache(p => p.Name)) - using (var results = new ChangeSetAggregator(source.Connect() - .Transform((Func) (p => null), - new ParallelisationOptions(ParallelType.Parallelise)))) - { - source.AddOrUpdate(new Person("Adult1", 50)); + const string key = "Adult1"; + var newPerson = new Person(key, 50); + var updated = new Person(key, 51); - results.Messages.Count.Should().Be(1, "Should be 1 updates"); - results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - results.Data.Items.First().Should().Be(null, "Should be same person"); - } + _source.AddOrUpdate(newPerson); + _source.AddOrUpdate(updated); + + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); + _results.Messages[1].Updates.Should().Be(1, "Should be 1 update"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformManyFixture.cs b/src/DynamicData.Tests/Cache/TransformManyFixture.cs index 2cee14abb..642fae144 100644 --- a/src/DynamicData.Tests/Cache/TransformManyFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformManyFixture.cs @@ -1,25 +1,40 @@ - -using System; +using System; + using DynamicData.Tests.Domain; using DynamicData.Tests.Utilities; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class TransformManyFixture: IDisposable + public class TransformManyFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; - public TransformManyFixture() + private readonly ISourceCache _source; + + public TransformManyFixture() { _source = new SourceCache(p => p.Key); - _results = _source.Connect().TransformMany(p => p.Relations.RecursiveSelect(r => r.Relations), p => p.Name) - .IgnoreUpdateWhen((current, previous) => current.Name == previous.Name) - .AsAggregator(); + _results = _source.Connect().TransformMany(p => p.Relations.RecursiveSelect(r => r.Relations), p => p.Name).IgnoreUpdateWhen((current, previous) => current.Name == previous.Name).AsAggregator(); + } + + [Fact] + public void ChildrenAreRemovedWhenParentIsRemoved() + { + var frientofchild1 = new PersonWithRelations("Friend1", 10); + var child1 = new PersonWithRelations("Child1", 10, new[] { frientofchild1 }); + var child2 = new PersonWithRelations("Child2", 8); + var child3 = new PersonWithRelations("Child3", 8); + var mother = new PersonWithRelations("Mother", 35, new[] { child1, child2, child3 }); + // var father = new PersonWithRelations("Father", 35, new[] {child1, child2, child3, mother}); + + _source.AddOrUpdate(mother); + _source.Remove(mother); + _results.Data.Count.Should().Be(0, "Should be 4 in the cache"); } public void Dispose() @@ -46,20 +61,5 @@ public void RecursiveChildrenCanBeAdded() _results.Data.Lookup("Child3").HasValue.Should().BeTrue(); _results.Data.Lookup("Friend1").HasValue.Should().BeTrue(); } - - [Fact] - public void ChildrenAreRemovedWhenParentIsRemoved() - { - var frientofchild1 = new PersonWithRelations("Friend1", 10); - var child1 = new PersonWithRelations("Child1", 10, new[] {frientofchild1}); - var child2 = new PersonWithRelations("Child2", 8); - var child3 = new PersonWithRelations("Child3", 8); - var mother = new PersonWithRelations("Mother", 35, new[] {child1, child2, child3}); - // var father = new PersonWithRelations("Father", 35, new[] {child1, child2, child3, mother}); - - _source.AddOrUpdate(mother); - _source.Remove(mother); - _results.Data.Count.Should().Be(0, "Should be 4 in the cache"); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs b/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs index 5560d0232..b472b776f 100644 --- a/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs @@ -4,13 +4,15 @@ using System.Diagnostics; using System.Linq; using System.Reactive.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class TransformManyObservableCollectionFixture { [Fact] @@ -19,51 +21,49 @@ public void FlattenObservableCollection() var children = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); int childIndex = 0; - var parents = Enumerable.Range(1, 50) - .Select(i => - { - var parent = new Parent(i, new[] + var parents = Enumerable.Range(1, 50).Select( + i => { - children[childIndex], - children[childIndex + 1] - }); - - childIndex = childIndex + 2; - return parent; - }).ToArray(); - - using (var source = new SourceCache(x => x.Id)) - using (var aggregator = source.Connect() - .TransformMany(p => p.Children, c => c.Name) - .AsAggregator()) - { - source.AddOrUpdate(parents); - - aggregator.Data.Count.Should().Be(100); - - //add a child to an observable collection and check the new item is added - parents[0].Children.Add(new Person("NewlyAddded", 100)); - aggregator.Data.Count.Should().Be(101); - - ////remove first parent and check children have gone - source.RemoveKey(1); - aggregator.Data.Count.Should().Be(98); - - //check items can be cleared and then added back in - var childrenInZero = parents[1].Children.ToArray(); - parents[1].Children.Clear(); - aggregator.Data.Count.Should().Be(96); - parents[1].Children.AddRange(childrenInZero); - aggregator.Data.Count.Should().Be(98); - - //replace produces an update - var replacedChild = parents[1].Children[0]; - parents[1].Children[0] = new Person("Replacement", 100); - aggregator.Data.Count.Should().Be(98); - - aggregator.Data.Lookup(replacedChild.Key).HasValue.Should().BeFalse(); - aggregator.Data.Lookup("Replacement").HasValue.Should().BeTrue(); - } + var parent = new Parent( + i, + new[] + { + children[childIndex], + children[childIndex + 1] + }); + + childIndex += 2; + return parent; + }).ToArray(); + + using var source = new SourceCache(x => x.Id); + using var aggregator = source.Connect().TransformMany(p => p.Children, c => c.Name).AsAggregator(); + source.AddOrUpdate(parents); + + aggregator.Data.Count.Should().Be(100); + + //add a child to an observable collection and check the new item is added + parents[0].Children.Add(new Person("NewlyAddded", 100)); + aggregator.Data.Count.Should().Be(101); + + ////remove first parent and check children have gone + source.RemoveKey(1); + aggregator.Data.Count.Should().Be(98); + + //check items can be cleared and then added back in + var childrenInZero = parents[1].Children.ToArray(); + parents[1].Children.Clear(); + aggregator.Data.Count.Should().Be(96); + parents[1].Children.AddRange(childrenInZero); + aggregator.Data.Count.Should().Be(98); + + //replace produces an update + var replacedChild = parents[1].Children[0]; + parents[1].Children[0] = new Person("Replacement", 100); + aggregator.Data.Count.Should().Be(98); + + aggregator.Data.Lookup(replacedChild.Key).HasValue.Should().BeFalse(); + aggregator.Data.Lookup("Replacement").HasValue.Should().BeTrue(); } [Fact] @@ -72,51 +72,67 @@ public void FlattenReadOnlyObservableCollection() var children = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); int childIndex = 0; - var parents = Enumerable.Range(1, 50) - .Select(i => - { - var parent = new Parent(i, new[] + var parents = Enumerable.Range(1, 50).Select( + i => { - children[childIndex], - children[childIndex + 1] - }); - - childIndex = childIndex + 2; - return parent; - }).ToArray(); - - using (var source = new SourceCache(x => x.Id)) - using (var aggregator = source.Connect() - .TransformMany(p => p.ChildrenReadonly, c => c.Name) - .AsAggregator()) - { - source.AddOrUpdate(parents); - - aggregator.Data.Count.Should().Be(100); + var parent = new Parent( + i, + new[] + { + children[childIndex], + children[childIndex + 1] + }); + + childIndex += 2; + return parent; + }).ToArray(); + + using var source = new SourceCache(x => x.Id); + using var aggregator = source.Connect().TransformMany(p => p.ChildrenReadonly, c => c.Name).AsAggregator(); + source.AddOrUpdate(parents); + + aggregator.Data.Count.Should().Be(100); + + //add a child to an observable collection and check the new item is added + parents[0].Children.Add(new Person("NewlyAddded", 100)); + aggregator.Data.Count.Should().Be(101); + + ////remove first parent and check children have gone + source.RemoveKey(1); + aggregator.Data.Count.Should().Be(98); + + //check items can be cleared and then added back in + var childrenInZero = parents[1].Children.ToArray(); + parents[1].Children.Clear(); + aggregator.Data.Count.Should().Be(96); + parents[1].Children.AddRange(childrenInZero); + aggregator.Data.Count.Should().Be(98); + + //replace produces an update + var replacedChild = parents[1].Children[0]; + parents[1].Children[0] = new Person("Replacement", 100); + aggregator.Data.Count.Should().Be(98); + + aggregator.Data.Lookup(replacedChild.Key).HasValue.Should().BeFalse(); + aggregator.Data.Lookup("Replacement").HasValue.Should().BeTrue(); + } - //add a child to an observable collection and check the new item is added - parents[0].Children.Add(new Person("NewlyAddded", 100)); - aggregator.Data.Count.Should().Be(101); + [Fact] + public void ObservableCollectionWithoutInitialData() + { + using var parents = new SourceCache(d => d.Id); + var collection = parents.Connect().TransformMany(d => d.Children, p => p.Name).AsObservableCache(); - ////remove first parent and check children have gone - source.RemoveKey(1); - aggregator.Data.Count.Should().Be(98); + var parent = new Parent(1); + parents.AddOrUpdate(parent); - //check items can be cleared and then added back in - var childrenInZero = parents[1].Children.ToArray(); - parents[1].Children.Clear(); - aggregator.Data.Count.Should().Be(96); - parents[1].Children.AddRange(childrenInZero); - aggregator.Data.Count.Should().Be(98); + collection.Count.Should().Be(0); - //replace produces an update - var replacedChild = parents[1].Children[0]; - parents[1].Children[0] = new Person("Replacement", 100); - aggregator.Data.Count.Should().Be(98); + parent.Children.Add(new Person("child1", 1)); + collection.Count.Should().Be(1); - aggregator.Data.Lookup(replacedChild.Key).HasValue.Should().BeFalse(); - aggregator.Data.Lookup("Replacement").HasValue.Should().BeTrue(); - } + parent.Children.Add(new Person("child2", 2)); + collection.Count.Should().Be(2); } [Fact] @@ -126,83 +142,49 @@ public void Perf() var children = Enumerable.Range(1, 10000).Select(i => new Person("Name" + i, i)).ToArray(); int childIndex = 0; - var parents = Enumerable.Range(1, 5000) - .Select(i => - { - var parent = new Parent(i, new[] + var parents = Enumerable.Range(1, 5000).Select( + i => { - children[childIndex], - children[childIndex + 1] - }); - - childIndex = childIndex + 2; - return parent; - }).ToArray(); + var parent = new Parent( + i, + new[] + { + children[childIndex], + children[childIndex + 1] + }); + + childIndex += 2; + return parent; + }).ToArray(); var sw = new Stopwatch(); - using (var source = new SourceCache(x => x.Id)) - using (var sut = source.Connect() - .Do(_ => sw.Start()) - .TransformMany(p => p.Children, c => c.Name) - .Do(_ => sw.Stop()) - .Subscribe(c => Console.WriteLine($"Changes = {c.Count:N0}"))) - { - source.AddOrUpdate(parents); - Console.WriteLine($"{sw.ElapsedMilliseconds}"); - } - } - - [Fact] - public void ObservableCollectionWithoutInitialData() - { - using (var parents = new SourceCache(d => d.Id)) - { - var collection = parents.Connect() - .TransformMany(d => d.Children, p => p.Name) - .AsObservableCache(); - - var parent = new Parent(1); - parents.AddOrUpdate(parent); - - collection.Count.Should().Be(0); - - parent.Children.Add(new Person("child1", 1)); - collection.Count.Should().Be(1); - - parent.Children.Add(new Person("child2", 2)); - collection.Count.Should().Be(2); - } + using var source = new SourceCache(x => x.Id); + using var sut = source.Connect().Do(_ => sw.Start()).TransformMany(p => p.Children, c => c.Name).Do(_ => sw.Stop()).Subscribe(c => Console.WriteLine($"Changes = {c.Count:N0}")); + source.AddOrUpdate(parents); + Console.WriteLine($"{sw.ElapsedMilliseconds}"); } [Fact] public void ReadOnlyObservableCollectionWithoutInitialData() { - using (var parents = new SourceCache(d => d.Id)) - { - var collection = parents.Connect() - .TransformMany(d => d.ChildrenReadonly, p => p.Name) - .AsObservableCache(); + using var parents = new SourceCache(d => d.Id); + var collection = parents.Connect().TransformMany(d => d.ChildrenReadonly, p => p.Name).AsObservableCache(); - var parent = new Parent(1); - parents.AddOrUpdate(parent); + var parent = new Parent(1); + parents.AddOrUpdate(parent); - collection.Count.Should().Be(0); + collection.Count.Should().Be(0); - parent.Children.Add(new Person("child1", 1)); - collection.Count.Should().Be(1); + parent.Children.Add(new Person("child1", 1)); + collection.Count.Should().Be(1); - parent.Children.Add(new Person("child2", 2)); - collection.Count.Should().Be(2); - } + parent.Children.Add(new Person("child2", 2)); + collection.Count.Should().Be(2); } private class Parent { - public int Id { get; } - public ObservableCollection Children { get; } - public ReadOnlyObservableCollection ChildrenReadonly { get; } - public Parent(int id, IEnumerable children) { Id = id; @@ -216,6 +198,12 @@ public Parent(int id) Children = new ObservableCollection(); ChildrenReadonly = new ReadOnlyObservableCollection(Children); } + + public ObservableCollection Children { get; } + + public ReadOnlyObservableCollection ChildrenReadonly { get; } + + public int Id { get; } } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformManyRefreshFixture.cs b/src/DynamicData.Tests/Cache/TransformManyRefreshFixture.cs index 267ffa9ce..c4c8f06dd 100644 --- a/src/DynamicData.Tests/Cache/TransformManyRefreshFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformManyRefreshFixture.cs @@ -1,31 +1,25 @@ -using DynamicData.Tests.Domain; -using FluentAssertions; -using System; +using System; using System.Collections.Generic; + +using DynamicData.Tests.Domain; + +using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class TransformManyRefreshFixture: IDisposable + public class TransformManyRefreshFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; + private readonly ISourceCache _source; + public TransformManyRefreshFixture() { _source = new SourceCache(p => p.Key); - _results = _source.Connect() - .AutoRefresh() - .TransformMany(p => p.Friends, p => p.Name) - .AsAggregator(); - } - - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); + _results = _source.Connect().AutoRefresh().TransformMany(p => p.Friends, p => p.Name).AsAggregator(); } [Fact] @@ -35,10 +29,10 @@ public void AutoRefresh() _source.AddOrUpdate(person); person.Friends = new[] - { - new PersonWithFriends("Friend1", 40), - new PersonWithFriends("Friend2", 45) - }; + { + new PersonWithFriends("Friend1", 40), + new PersonWithFriends("Friend2", 45) + }; _results.Data.Count.Should().Be(2, "Should be 2 in the cache"); _results.Data.Lookup("Friend1").HasValue.Should().BeTrue(); @@ -48,7 +42,7 @@ public void AutoRefresh() [Fact] public void AutoRefreshOnOtherProperty() { - var friends = new List { new PersonWithFriends("Friend1", 40) }; + var friends = new List { new("Friend1", 40) }; var person = new PersonWithFriends("Person", 50, friends); _source.AddOrUpdate(person); @@ -63,7 +57,7 @@ public void AutoRefreshOnOtherProperty() [Fact] public void DirectRefresh() { - var friends = new List {new PersonWithFriends("Friend1", 40)}; + var friends = new List { new("Friend1", 40) }; var person = new PersonWithFriends("Person", 50, friends); _source.AddOrUpdate(person); @@ -75,5 +69,10 @@ public void DirectRefresh() _results.Data.Lookup("Friend2").HasValue.Should().BeTrue(); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformManySimpleFixture.cs b/src/DynamicData.Tests/Cache/TransformManySimpleFixture.cs index 405dd5c33..14610723c 100644 --- a/src/DynamicData.Tests/Cache/TransformManySimpleFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformManySimpleFixture.cs @@ -1,39 +1,38 @@ -using DynamicData.Tests.Domain; +using System; + +using DynamicData.Tests.Domain; + using FluentAssertions; -using System; + using Xunit; namespace DynamicData.Tests.Cache { - - public class TransformManySimpleFixture: IDisposable + public class TransformManySimpleFixture : IDisposable { - private readonly ISourceCache _source; private readonly ChangeSetAggregator _results; - public TransformManySimpleFixture() + private readonly ISourceCache _source; + + public TransformManySimpleFixture() { _source = new SourceCache(p => p.Key); - _results = _source.Connect().TransformMany(p => p.Relations, p => p.Name) - .AsAggregator(); - } - - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); + _results = _source.Connect().TransformMany(p => p.Relations, p => p.Name).AsAggregator(); } [Fact] public void Adds() { - var parent = new PersonWithChildren("parent", 50, new Person[] - { - new Person("Child1", 1), - new Person("Child2", 2), - new Person("Child3", 3) - }); + var parent = new PersonWithChildren( + "parent", + 50, + new Person[] + { + new("Child1", 1), + new("Child2", 2), + new("Child3", 3) + }); _source.AddOrUpdate(parent); _results.Data.Count.Should().Be(3, "Should be 4 in the cache"); @@ -42,13 +41,22 @@ public void Adds() _results.Data.Lookup("Child3").HasValue.Should().BeTrue(); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + [Fact] public void Remove() { - var parent = new PersonWithChildren("parent", 50, new Person[] - { - new Person("Child1", 1), new Person("Child2", 2), new Person("Child3", 3) - }); + var parent = new PersonWithChildren( + "parent", + 50, + new Person[] + { + new("Child1", 1), new("Child2", 2), new("Child3", 3) + }); _source.AddOrUpdate(parent); _source.Remove(parent); _results.Data.Count.Should().Be(0, "Should be 4 in the cache"); @@ -57,16 +65,22 @@ public void Remove() [Fact] public void RemovewithIncompleteChildren() { - var parent1 = new PersonWithChildren("parent", 50, new Person[] - { - new Person("Child1", 1), new Person("Child2", 2), new Person("Child3", 3) - }); + var parent1 = new PersonWithChildren( + "parent", + 50, + new Person[] + { + new("Child1", 1), new("Child2", 2), new("Child3", 3) + }); _source.AddOrUpdate(parent1); - var parent2 = new PersonWithChildren("parent", 50, new Person[] - { - new Person("Child1", 1), new Person("Child3", 3) - }); + var parent2 = new PersonWithChildren( + "parent", + 50, + new Person[] + { + new("Child1", 1), new("Child3", 3) + }); _source.Remove(parent2); _results.Data.Count.Should().Be(0, "Should be 0 in the cache"); } @@ -74,16 +88,22 @@ public void RemovewithIncompleteChildren() [Fact] public void UpdateWithLessChildren() { - var parent1 = new PersonWithChildren("parent", 50, new Person[] - { - new Person("Child1", 1), new Person("Child2", 2), new Person("Child3", 3) - }); + var parent1 = new PersonWithChildren( + "parent", + 50, + new Person[] + { + new("Child1", 1), new("Child2", 2), new("Child3", 3) + }); _source.AddOrUpdate(parent1); - var parent2 = new PersonWithChildren("parent", 50, new Person[] - { - new Person("Child1", 1), new Person("Child3", 3), - }); + var parent2 = new PersonWithChildren( + "parent", + 50, + new Person[] + { + new("Child1", 1), new("Child3", 3), + }); _source.AddOrUpdate(parent2); _results.Data.Count.Should().Be(2, "Should be 2 in the cache"); _results.Data.Lookup("Child1").HasValue.Should().BeTrue(); @@ -93,22 +113,27 @@ public void UpdateWithLessChildren() [Fact] public void UpdateWithMultipleChanges() { - var parent1 = new PersonWithChildren("parent", 50, new Person[] - { - new Person("Child1", 1), new Person("Child2", 2), new Person("Child3", 3) - }); + var parent1 = new PersonWithChildren( + "parent", + 50, + new Person[] + { + new("Child1", 1), new("Child2", 2), new("Child3", 3) + }); _source.AddOrUpdate(parent1); - var parent2 = new PersonWithChildren("parent", 50, new Person[] - { - new Person("Child1", 1), new Person("Child3", 3), new Person("Child5", 3), - }); + var parent2 = new PersonWithChildren( + "parent", + 50, + new Person[] + { + new("Child1", 1), new("Child3", 3), new("Child5", 3), + }); _source.AddOrUpdate(parent2); _results.Data.Count.Should().Be(3, "Should be 2 in the cache"); _results.Data.Lookup("Child1").HasValue.Should().BeTrue(); _results.Data.Lookup("Child3").HasValue.Should().BeTrue(); _results.Data.Lookup("Child5").HasValue.Should().BeTrue(); } - } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs b/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs index 3b7de5f72..54facbd54 100644 --- a/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformSafeAsyncFixture.cs @@ -5,9 +5,12 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache @@ -21,22 +24,20 @@ public void ReTransformAll() var people = Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray(); var forceTransform = new Subject(); - using (var stub = new TransformStub(forceTransform)) - { - stub.Source.AddOrUpdate(people); - forceTransform.OnNext(Unit.Default); + using var stub = new TransformStub(forceTransform); + stub.Source.AddOrUpdate(people); + forceTransform.OnNext(Unit.Default); - stub.Results.Messages.Count.Should().Be(2); - stub.Results.Messages[1].Updates.Should().Be(10); + stub.Results.Messages.Count.Should().Be(2); + stub.Results.Messages[1].Updates.Should().Be(10); - for (int i = 1; i <= 10; i++) - { - var original = stub.Results.Messages[0].ElementAt(i - 1).Current; - var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; + for (int i = 1; i <= 10; i++) + { + var original = stub.Results.Messages[0].ElementAt(i - 1).Current; + var updated = stub.Results.Messages[1].ElementAt(i - 1).Current; - updated.Should().Be(original); - ReferenceEquals(original, updated).Should().BeFalse(); - } + updated.Should().Be(original); + ReferenceEquals(original, updated).Should().BeFalse(); } } @@ -189,7 +190,7 @@ public void ReTransformAll() // stub.Results.Error.Should().BeNull(); - // Exception error = null; + // Exception? error = null; // stub.Source.Connect() // .Subscribe(changes => { }, ex => error = ex); @@ -202,87 +203,86 @@ public void ReTransformAll() private class TransformStub : IDisposable { - public ISourceCache Source { get; } = new SourceCache(p => p.Name); - public ChangeSetAggregator Results { get; } - - public Func< Person, Task> TransformFactory { get; } - - public IList> HandledErrors { get; } = new List>(); - public TransformStub() { TransformFactory = (p) => - { - var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); - return Task.FromResult(result); - }; - - Results = new ChangeSetAggregator - ( - Source.Connect().TransformSafeAsync(TransformFactory, ErrorHandler) - ); + { + var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); + return Task.FromResult(result); + }; + + Results = new ChangeSetAggregator(Source.Connect().TransformSafeAsync(TransformFactory, ErrorHandler)); } public TransformStub(Func factory) { TransformFactory = (p) => - { - var result = factory(p); - return Task.FromResult(result); - }; - - Results = new ChangeSetAggregator - ( - Source.Connect().TransformSafeAsync(TransformFactory, ErrorHandler) - ); + { + var result = factory(p); + return Task.FromResult(result); + }; + + Results = new ChangeSetAggregator(Source.Connect().TransformSafeAsync(TransformFactory, ErrorHandler)); } public TransformStub(IObservable retransformer) { TransformFactory = (p) => - { - var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); - return Task.FromResult(result); - }; - - Results = new ChangeSetAggregator - ( - Source.Connect().TransformSafeAsync(TransformFactory, ErrorHandler, retransformer.Select(x => { - bool Transformer(Person p, string key) => true; - return (Func) Transformer; - })) - ); + var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); + return Task.FromResult(result); + }; + + Results = new ChangeSetAggregator( + Source.Connect().TransformSafeAsync( + TransformFactory, + ErrorHandler, + retransformer.Select( + x => + { + bool Transformer(Person p, string key) => true; + return (Func)Transformer; + }))); } public TransformStub(IObservable> retransformer) { TransformFactory = (p) => - { - var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); - return Task.FromResult(result); - }; - - Results = new ChangeSetAggregator - ( - Source.Connect().TransformSafeAsync(TransformFactory, ErrorHandler, retransformer.Select(selector => { - bool Transformed(Person p, string key) => selector(p); - return (Func) Transformed; - })) - ); + var result = new PersonWithGender(p, p.Age % 2 == 0 ? "M" : "F"); + return Task.FromResult(result); + }; + + Results = new ChangeSetAggregator( + Source.Connect().TransformSafeAsync( + TransformFactory, + ErrorHandler, + retransformer.Select( + selector => + { + bool Transformed(Person p, string key) => selector(p); + return (Func)Transformed; + }))); } - private void ErrorHandler(Error error) - { - HandledErrors.Add(error); - } + public IList> HandledErrors { get; } = new List>(); + + public ChangeSetAggregator Results { get; } + + public ISourceCache Source { get; } = new SourceCache(p => p.Name); + + public Func> TransformFactory { get; } public void Dispose() { Source.Dispose(); Results.Dispose(); } + + private void ErrorHandler(Error error) + { + HandledErrors.Add(error); + } } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformSafeFixture.cs b/src/DynamicData.Tests/Cache/TransformSafeFixture.cs index 3feced478..2f3e8ee7e 100644 --- a/src/DynamicData.Tests/Cache/TransformSafeFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformSafeFixture.cs @@ -1,32 +1,36 @@ using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class TransformSafeFixture: IDisposable + public class TransformSafeFixture : IDisposable { - private ISourceCache _source; - private ChangeSetAggregator _results; - private IList> _errors; - private readonly Func _transformFactory = p => - { - if (p.Age % 3 == 0) { - throw new Exception($"Cannot transform {p}"); - } + if (p.Age % 3 == 0) + { + throw new Exception($"Cannot transform {p}"); + } + + string gender = p.Age % 2 == 0 ? "M" : "F"; + return new PersonWithGender(p, gender); + }; + + private readonly IList> _errors; + + private readonly ChangeSetAggregator _results; - string gender = p.Age % 2 == 0 ? "M" : "F"; - return new PersonWithGender(p, gender); - }; + private readonly ISourceCache _source; - public TransformSafeFixture() + public TransformSafeFixture() { _source = new SourceCache(p => p.Key); _errors = new List>(); @@ -35,10 +39,14 @@ public TransformSafeFixture() _results = new ChangeSetAggregator(safeTransform); } - public void Dispose() + [Fact] + public void AddWithError() { - _source.Dispose(); - _results.Dispose(); + var person = new Person("Person", 3); + _source.AddOrUpdate(person); + + _errors.Count.Should().Be(1, "Should be 1 error reported"); + _results.Messages.Count.Should().Be(0, "Should be no messages"); } [Fact] @@ -52,33 +60,10 @@ public void AddWithNoError() _results.Data.Items.First().Should().Be(_transformFactory(person), "Should be same person"); } - [Fact] - public void AddWithError() - { - var person = new Person("Person", 3); - _source.AddOrUpdate(person); - - _errors.Count.Should().Be(1, "Should be 1 error reported"); - _results.Messages.Count.Should().Be(0, "Should be no messages"); - } - - [Fact] - public void UpdateSucessively() + public void Dispose() { - const string key = "Adult1"; - var update1 = new Person(key, 1); - var update2 = new Person(key, 2); - var update3 = new Person(key, 3); - - _source.AddOrUpdate(update1); - _source.AddOrUpdate(update2); - _source.AddOrUpdate(update3); - - _errors.Count.Should().Be(1, "Should be 1 error reported"); - _results.Messages.Count.Should().Be(2, "Should be 2 messages"); - - _results.Data.Count.Should().Be(1, "Should 1 item in the cache"); - _results.Data.Items.First().Should().Be(_transformFactory(update2), "Change 2 shoud be the only item cached"); + _source.Dispose(); + _results.Dispose(); } [Fact] @@ -89,12 +74,13 @@ public void UpdateBatch() var update2 = new Person(key, 2); var update3 = new Person(key, 3); - _source.Edit(innerCache => - { - innerCache.AddOrUpdate(update1); - innerCache.AddOrUpdate(update2); - innerCache.AddOrUpdate(update3); - }); + _source.Edit( + innerCache => + { + innerCache.AddOrUpdate(update1); + innerCache.AddOrUpdate(update2); + innerCache.AddOrUpdate(update3); + }); _errors.Count.Should().Be(1, "Should be 1 error reported"); _results.Messages.Count.Should().Be(1, "Should be 1 messages"); @@ -118,5 +104,24 @@ public void UpdateBatchAndClear() _results.Messages[1].Removes.Should().Be(67, "Should be 67 removes"); _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + + [Fact] + public void UpdateSucessively() + { + const string key = "Adult1"; + var update1 = new Person(key, 1); + var update2 = new Person(key, 2); + var update3 = new Person(key, 3); + + _source.AddOrUpdate(update1); + _source.AddOrUpdate(update2); + _source.AddOrUpdate(update3); + + _errors.Count.Should().Be(1, "Should be 1 error reported"); + _results.Messages.Count.Should().Be(2, "Should be 2 messages"); + + _results.Data.Count.Should().Be(1, "Should 1 item in the cache"); + _results.Data.Items.First().Should().Be(_transformFactory(update2), "Change 2 shoud be the only item cached"); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformSafeParallelFixture.cs b/src/DynamicData.Tests/Cache/TransformSafeParallelFixture.cs index c43d4bfdd..63d95aec8 100644 --- a/src/DynamicData.Tests/Cache/TransformSafeParallelFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformSafeParallelFixture.cs @@ -1,32 +1,36 @@ using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class TransformSafeParallelFixture: IDisposable + public class TransformSafeParallelFixture : IDisposable { - private readonly ISourceCache _source; - private readonly ChangeSetAggregator _results; private readonly IList> _errors; + private readonly ChangeSetAggregator _results; + + private readonly ISourceCache _source; + private readonly Func _transformFactory = p => - { - if (p.Age % 3 == 0) { - throw new Exception($"Cannot transform {p}"); - } + if (p.Age % 3 == 0) + { + throw new Exception($"Cannot transform {p}"); + } - string gender = p.Age % 2 == 0 ? "M" : "F"; - return new PersonWithGender(p, gender); - }; + string gender = p.Age % 2 == 0 ? "M" : "F"; + return new PersonWithGender(p, gender); + }; - public TransformSafeParallelFixture() + public TransformSafeParallelFixture() { _source = new SourceCache(p => p.Name); _errors = new List>(); @@ -35,10 +39,14 @@ public TransformSafeParallelFixture() _results = new ChangeSetAggregator(safeTransform); } - public void Dispose() + [Fact] + public void AddWithError() { - _source.Dispose(); - _results.Dispose(); + var person = new Person("Person", 3); + _source.AddOrUpdate(person); + + _errors.Count.Should().Be(1, "Should be 1 error reported"); + _results.Messages.Count.Should().Be(0, "Should be no messages"); } [Fact] @@ -52,33 +60,10 @@ public void AddWithNoError() _results.Data.Items.First().Should().Be(_transformFactory(person), "Should be same person"); } - [Fact] - public void AddWithError() - { - var person = new Person("Person", 3); - _source.AddOrUpdate(person); - - _errors.Count.Should().Be(1, "Should be 1 error reported"); - _results.Messages.Count.Should().Be(0, "Should be no messages"); - } - - [Fact] - public void UpdateSucessively() + public void Dispose() { - const string key = "Adult1"; - var update1 = new Person(key, 1); - var update2 = new Person(key, 2); - var update3 = new Person(key, 3); - - _source.AddOrUpdate(update1); - _source.AddOrUpdate(update2); - _source.AddOrUpdate(update3); - - _errors.Count.Should().Be(1, "Should be 1 error reported"); - _results.Messages.Count.Should().Be(2, "Should be 2 messages"); - - _results.Data.Count.Should().Be(1, "Should 1 item in the cache"); - _results.Data.Items.First().Should().Be(_transformFactory(update2), "Change 2 shoud be the only item cached"); + _source.Dispose(); + _results.Dispose(); } [Fact] @@ -89,12 +74,13 @@ public void UpdateBatch() var update2 = new Person(key, 2); var update3 = new Person(key, 3); - _source.Edit(updater => - { - updater.AddOrUpdate(update1); - updater.AddOrUpdate(update2); - updater.AddOrUpdate(update3); - }); + _source.Edit( + updater => + { + updater.AddOrUpdate(update1); + updater.AddOrUpdate(update2); + updater.AddOrUpdate(update3); + }); _errors.Count.Should().Be(1, "Should be 1 error reported"); _results.Messages.Count.Should().Be(1, "Should be 1 messages"); @@ -118,5 +104,24 @@ public void UpdateBatchAndClear() _results.Messages[1].Removes.Should().Be(67, "Should be 67 removes"); _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + + [Fact] + public void UpdateSucessively() + { + const string key = "Adult1"; + var update1 = new Person(key, 1); + var update2 = new Person(key, 2); + var update3 = new Person(key, 3); + + _source.AddOrUpdate(update1); + _source.AddOrUpdate(update2); + _source.AddOrUpdate(update3); + + _errors.Count.Should().Be(1, "Should be 1 error reported"); + _results.Messages.Count.Should().Be(2, "Should be 2 messages"); + + _results.Data.Count.Should().Be(1, "Should 1 item in the cache"); + _results.Data.Items.First().Should().Be(_transformFactory(update2), "Change 2 shoud be the only item cached"); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformTreeFixture.cs b/src/DynamicData.Tests/Cache/TransformTreeFixture.cs index f115ea4dd..d08734a09 100644 --- a/src/DynamicData.Tests/Cache/TransformTreeFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformTreeFixture.cs @@ -2,108 +2,28 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class TransformTreeFixture: IDisposable + public class TransformTreeFixture : IDisposable { - private readonly ISourceCache _sourceCache; - private readonly IObservableCache, int> _result; private readonly BehaviorSubject, bool>> _filter; - public TransformTreeFixture() - { - _sourceCache = new SourceCache(e => e.Id); - - _filter = new BehaviorSubject, bool>>(n=>n.IsRoot); - - _result = _sourceCache.Connect() - .TransformToTree(e => e.BossId, _filter) - .AsObservableCache(); - } - - public void Dispose() - { - _sourceCache.Dispose(); - _result.Dispose(); - } - - [Fact] - public void BuildTreeFromMixedData() - { - _sourceCache.AddOrUpdate(CreateEmployees()); - _result.Count.Should().Be(2); - - var firstNode = _result.Items.First(); - firstNode.Children.Count.Should().Be(3); - - var secondNode = _result.Items.Skip(1).First(); - secondNode.Children.Count.Should().Be(0); - } - - [Fact] - public void UpdateAParentNode() - { - _sourceCache.AddOrUpdate(CreateEmployees()); - - var changed = new EmployeeDto(1) - { - BossId = 0, - Name = "Employee 1 (with name change)" - }; - - _sourceCache.AddOrUpdate(changed); - _result.Count.Should().Be(2); - - var firstNode = _result.Items.First(); - firstNode.Children.Count.Should().Be(3); - firstNode.Item.Name.Should().Be(changed.Name); - } - - [Fact] - public void UpdateChildNode() - { - _sourceCache.AddOrUpdate(CreateEmployees()); - - var changed = new EmployeeDto(2) - { - BossId = 1, - Name = "Employee 2 (with name change)" - }; - - _sourceCache.AddOrUpdate(changed); - _result.Count.Should().Be(2); - - var changedNode = _result.Items.First().Children.Items.First(); - - changedNode.Parent.Value.Item.Id.Should().Be(1); - changedNode.Children.Count.Should().Be(1); - changed.Name.Should().Be(changed.Name); - } - - [Fact] - public void RemoveARootNodeWillPushOrphansUpTheHierachy() - { - _sourceCache.AddOrUpdate(CreateEmployees()); - _sourceCache.Remove(1); + private readonly IObservableCache, int> _result; - //we expect the original children nodes to be pushed up become new roots - _result.Count.Should().Be(4); - } + private readonly ISourceCache _sourceCache; - [Fact] - public void RemoveAChildNodeWillPushOrphansUpTheHierachy() + public TransformTreeFixture() { - _sourceCache.AddOrUpdate(CreateEmployees()); - _sourceCache.Remove(4); + _sourceCache = new SourceCache(e => e.Id); - //we expect the children of node 4 to be pushed up become new roots - _result.Count.Should().Be(3); + _filter = new BehaviorSubject, bool>>(n => n.IsRoot); - var thirdNode = _result.Items.Skip(2).First(); - thirdNode.Key.Should().Be(5); + _result = _sourceCache.Connect().TransformToTree(e => e.BossId, _filter).AsObservableCache(); } [Fact] @@ -155,16 +75,30 @@ public void AddMissingParent() emp12Node.Value.Children.Count.Should().Be(0); } + [Fact] + public void BuildTreeFromMixedData() + { + _sourceCache.AddOrUpdate(CreateEmployees()); + _result.Count.Should().Be(2); + + var firstNode = _result.Items.First(); + firstNode.Children.Count.Should().Be(3); + + var secondNode = _result.Items.Skip(1).First(); + secondNode.Children.Count.Should().Be(0); + } + [Fact] public void ChangeParent() { _sourceCache.AddOrUpdate(CreateEmployees()); - _sourceCache.AddOrUpdate(new EmployeeDto(4) - { - BossId = 1, - Name = "Employee4" - }); + _sourceCache.AddOrUpdate( + new EmployeeDto(4) + { + BossId = 1, + Name = "Employee4" + }); //if this throws, then employee 4 is no a child of boss 1 var emp4 = _result.Lookup(1).Value.Children.Lookup(4).Value; @@ -179,6 +113,75 @@ public void ChangeParent() emp3.Children.Lookup(4).HasValue.Should().BeFalse(); } + public void Dispose() + { + _sourceCache.Dispose(); + _result.Dispose(); + } + + [Fact] + public void RemoveAChildNodeWillPushOrphansUpTheHierachy() + { + _sourceCache.AddOrUpdate(CreateEmployees()); + _sourceCache.Remove(4); + + //we expect the children of node 4 to be pushed up become new roots + _result.Count.Should().Be(3); + + var thirdNode = _result.Items.Skip(2).First(); + thirdNode.Key.Should().Be(5); + } + + [Fact] + public void RemoveARootNodeWillPushOrphansUpTheHierachy() + { + _sourceCache.AddOrUpdate(CreateEmployees()); + _sourceCache.Remove(1); + + //we expect the original children nodes to be pushed up become new roots + _result.Count.Should().Be(4); + } + + [Fact] + public void UpdateAParentNode() + { + _sourceCache.AddOrUpdate(CreateEmployees()); + + var changed = new EmployeeDto(1) + { + BossId = 0, + Name = "Employee 1 (with name change)" + }; + + _sourceCache.AddOrUpdate(changed); + _result.Count.Should().Be(2); + + var firstNode = _result.Items.First(); + firstNode.Children.Count.Should().Be(3); + firstNode.Item.Name.Should().Be(changed.Name); + } + + [Fact] + public void UpdateChildNode() + { + _sourceCache.AddOrUpdate(CreateEmployees()); + + var changed = new EmployeeDto(2) + { + BossId = 1, + Name = "Employee 2 (with name change)" + }; + + _sourceCache.AddOrUpdate(changed); + _result.Count.Should().Be(2); + + var changedNode = _result.Items.First().Children.Items.First(); + + changedNode.Parent.Value.Item.Id.Should().Be(1); + changedNode.Children.Count.Should().Be(1); + changed.Name.Should().Be(changed.Name); + } + [Fact] public void UseCustomFilter() { @@ -199,57 +202,55 @@ public void UseCustomFilter() _result.Count.Should().Be(2); } - #region Employees - private IEnumerable CreateEmployees() { yield return new EmployeeDto(1) - { - BossId = 0, - Name = "Employee1" - }; + { + BossId = 0, + Name = "Employee1" + }; yield return new EmployeeDto(2) - { - BossId = 1, - Name = "Employee2" - }; + { + BossId = 1, + Name = "Employee2" + }; yield return new EmployeeDto(3) - { - BossId = 1, - Name = "Employee3" - }; + { + BossId = 1, + Name = "Employee3" + }; yield return new EmployeeDto(4) - { - BossId = 3, - Name = "Employee4" - }; + { + BossId = 3, + Name = "Employee4" + }; yield return new EmployeeDto(5) - { - BossId = 4, - Name = "Employee5" - }; + { + BossId = 4, + Name = "Employee5" + }; yield return new EmployeeDto(6) - { - BossId = 2, - Name = "Employee6" - }; + { + BossId = 2, + Name = "Employee6" + }; yield return new EmployeeDto(7) - { - BossId = 0, - Name = "Employee7" - }; + { + BossId = 0, + Name = "Employee7" + }; yield return new EmployeeDto(8) - { - BossId = 1, - Name = "Employee8" - }; + { + BossId = 1, + Name = "Employee8" + }; } public class EmployeeDto : IEquatable @@ -259,13 +260,23 @@ public EmployeeDto(int id) Id = id; } - public int Id { get; set; } public int BossId { get; set; } - public string Name { get; set; } - #region Equality Members + public int Id { get; set; } - public bool Equals(EmployeeDto other) + public string? Name { get; set; } + + public static bool operator ==(EmployeeDto left, EmployeeDto right) + { + return Equals(left, right); + } + + public static bool operator !=(EmployeeDto left, EmployeeDto right) + { + return !Equals(left, right); + } + + public bool Equals(EmployeeDto? other) { if (ReferenceEquals(null, other)) { @@ -280,7 +291,7 @@ public bool Equals(EmployeeDto other) return Id == other.Id; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -305,24 +316,10 @@ public override int GetHashCode() return Id; } - public static bool operator ==(EmployeeDto left, EmployeeDto right) - { - return Equals(left, right); - } - - public static bool operator !=(EmployeeDto left, EmployeeDto right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"Name: {Name}, Id: {Id}, BossId: {BossId}"; } } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TransformTreeWithRefreshFixture.cs b/src/DynamicData.Tests/Cache/TransformTreeWithRefreshFixture.cs index 667f82c12..9dfc9a359 100644 --- a/src/DynamicData.Tests/Cache/TransformTreeWithRefreshFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformTreeWithRefreshFixture.cs @@ -1,23 +1,24 @@ using System; using System.Collections.Generic; + using DynamicData.Binding; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - public class TransformTreeWithRefreshFixture: IDisposable + public class TransformTreeWithRefreshFixture : IDisposable { - private readonly ISourceCache _sourceCache; private readonly IObservableCache, int> _result; + private readonly ISourceCache _sourceCache; + public TransformTreeWithRefreshFixture() { _sourceCache = new SourceCache(e => e.Id); - _result = _sourceCache.Connect() - .AutoRefresh() - .TransformToTree(e => e.BossId) - .AsObservableCache(); + _result = _sourceCache.Connect().AutoRefresh().TransformToTree(e => e.BossId).AsObservableCache(); _sourceCache.AddOrUpdate(CreateEmployees()); } @@ -28,28 +29,18 @@ public void Dispose() } [Fact] - public void UpdateTreeWhenParentIdOfRootItemChangedToExistingId() - { - _sourceCache.Lookup(1).Value.BossId = 7; - - // node 1 added to node 7 children cache - var node1 = _result.Lookup(7).Value.Children.Lookup(1); - node1.HasValue.Should().BeTrue(); - node1.Value.IsRoot.Should().BeFalse(); - - // node 1 removed from root - _result.Lookup(1).HasValue.Should().BeFalse(); - } - - [Fact] - public void UpdateTreeWhenParentIdOfRootItemChangedToNonExistingId() + public void DoNotUpdateTreeWhenParentIdNotChanged() { - _sourceCache.Lookup(1).Value.BossId = 25; + _sourceCache.Lookup(1).Value.Name = "Employee11"; + _sourceCache.Lookup(2).Value.Name = "Employee22"; - // node 1 added to node 7 children cache var node1 = _result.Lookup(1); node1.HasValue.Should().BeTrue(); - node1.Value.IsRoot.Should().BeTrue(); + node1.Value.Parent.HasValue.Should().BeFalse(); + var node2 = node1.Value.Children.Lookup(2); + node2.HasValue.Should().BeTrue(); + node2.Value.Parent.HasValue.Should().BeTrue(); + node2.Value.Parent.Value.Key.Should().Be(1); } [Fact] @@ -81,100 +72,117 @@ public void UpdateTreeWhenParentIdOfNonRootItemChangedToNonExistingId() } [Fact] - public void DoNotUpdateTreeWhenParentIdNotChanged() + public void UpdateTreeWhenParentIdOfRootItemChangedToExistingId() { - _sourceCache.Lookup(1).Value.Name = "Employee11"; - _sourceCache.Lookup(2).Value.Name = "Employee22"; + _sourceCache.Lookup(1).Value.BossId = 7; - var node1 = _result.Lookup(1); + // node 1 added to node 7 children cache + var node1 = _result.Lookup(7).Value.Children.Lookup(1); node1.HasValue.Should().BeTrue(); - node1.Value.Parent.HasValue.Should().BeFalse(); - var node2 = node1.Value.Children.Lookup(2); - node2.HasValue.Should().BeTrue(); - node2.Value.Parent.HasValue.Should().BeTrue(); - node2.Value.Parent.Value.Key.Should().Be(1); + node1.Value.IsRoot.Should().BeFalse(); + + // node 1 removed from root + _result.Lookup(1).HasValue.Should().BeFalse(); } - #region Employees + [Fact] + public void UpdateTreeWhenParentIdOfRootItemChangedToNonExistingId() + { + _sourceCache.Lookup(1).Value.BossId = 25; + + // node 1 added to node 7 children cache + var node1 = _result.Lookup(1); + node1.HasValue.Should().BeTrue(); + node1.Value.IsRoot.Should().BeTrue(); + } private IEnumerable CreateEmployees() { yield return new EmployeeDto(1) - { - BossId = 0, - Name = "Employee1" - }; + { + BossId = 0, + Name = "Employee1" + }; yield return new EmployeeDto(2) - { - BossId = 1, - Name = "Employee2" - }; + { + BossId = 1, + Name = "Employee2" + }; yield return new EmployeeDto(3) - { - BossId = 1, - Name = "Employee3" - }; + { + BossId = 1, + Name = "Employee3" + }; yield return new EmployeeDto(4) - { - BossId = 3, - Name = "Employee4" - }; + { + BossId = 3, + Name = "Employee4" + }; yield return new EmployeeDto(5) - { - BossId = 4, - Name = "Employee5" - }; + { + BossId = 4, + Name = "Employee5" + }; yield return new EmployeeDto(6) - { - BossId = 2, - Name = "Employee6" - }; + { + BossId = 2, + Name = "Employee6" + }; yield return new EmployeeDto(7) - { - BossId = 0, - Name = "Employee7" - }; + { + BossId = 0, + Name = "Employee7" + }; yield return new EmployeeDto(8) - { - BossId = 1, - Name = "Employee8" - }; + { + BossId = 1, + Name = "Employee8" + }; } private class EmployeeDto : AbstractNotifyPropertyChanged, IEquatable { private int _bossId; - private string _name; + + private string? _name; public EmployeeDto(int id) { Id = id; } - public int Id { get; } - public int BossId { get => _bossId; set => SetAndRaise(ref _bossId, value); } - public string Name + public int Id { get; } + + public string? Name { get => _name; set => SetAndRaise(ref _name, value); } - #region Equality Members + public static bool operator ==(EmployeeDto left, EmployeeDto right) + { + return Equals(left, right); + } + + public static bool operator !=(EmployeeDto left, EmployeeDto right) + { + return !Equals(left, right); + } - public bool Equals(EmployeeDto other) + public bool Equals(EmployeeDto? other) { if (ReferenceEquals(null, other)) { @@ -189,7 +197,7 @@ public bool Equals(EmployeeDto other) return Id == other.Id; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -214,24 +222,10 @@ public override int GetHashCode() return Id; } - public static bool operator ==(EmployeeDto left, EmployeeDto right) - { - return Equals(left, right); - } - - public static bool operator !=(EmployeeDto left, EmployeeDto right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"Name: {Name}, Id: {Id}, BossId: {BossId}"; } } - - #endregion } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TrueForAllFixture.cs b/src/DynamicData.Tests/Cache/TrueForAllFixture.cs index 448ad7d84..1e9495a01 100644 --- a/src/DynamicData.Tests/Cache/TrueForAllFixture.cs +++ b/src/DynamicData.Tests/Cache/TrueForAllFixture.cs @@ -1,22 +1,23 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class TrueForAllFixture: IDisposable + public class TrueForAllFixture : IDisposable { - private readonly ISourceCache _source; private readonly IObservable _observable; - public TrueForAllFixture() + private readonly ISourceCache _source; + + public TrueForAllFixture() { _source = new SourceCache(p => p.Id); - _observable = _source.Connect() - .TrueForAll(o => o.Observable.StartWith(o.Value), (obj, invoked) => invoked); + _observable = _source.Connect().TrueForAll(o => o.Observable.StartWith(o.Value), (obj, invoked) => invoked); } public void Dispose() @@ -25,16 +26,22 @@ public void Dispose() } [Fact] - public void InitialItemReturnsFalseWhenObservaleHasNoValue() + public void InitialItemReturnsFalseWhenObservableHasNoValue() { - bool? valuereturned = null; - var subscribed = _observable.Subscribe(result => { valuereturned = result; }); + bool? valueReturned = null; + var subscribed = _observable.Subscribe(result => { valueReturned = result; }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); - valuereturned.HasValue.Should().BeTrue(); - valuereturned.Value.Should().Be(false, "The intial value should be false"); + valueReturned.HasValue.Should().BeTrue(); + + if (valueReturned is null) + { + throw new InvalidOperationException(nameof(valueReturned)); + } + + valueReturned.Value.Should().Be(false, "The initial value should be false"); subscribed.Dispose(); } @@ -42,22 +49,27 @@ public void InitialItemReturnsFalseWhenObservaleHasNoValue() [Fact] public void InlineObservableChangeProducesResult() { - bool? valuereturned = null; - var subscribed = _observable.Subscribe(result => { valuereturned = result; }); + bool? valueReturned = null; + var subscribed = _observable.Subscribe(result => { valueReturned = result; }); var item = new ObjectWithObservable(1); item.InvokeObservable(true); _source.AddOrUpdate(item); - valuereturned.Value.Should().Be(true, "Value should be true"); + if (valueReturned is null) + { + throw new InvalidOperationException(nameof(valueReturned)); + } + + valueReturned.Value.Should().Be(true, "Value should be true"); subscribed.Dispose(); } [Fact] public void MultipleValuesReturnTrue() { - bool? valuereturned = null; - var subscribed = _observable.Subscribe(result => { valuereturned = result; }); + bool? valueReturned = null; + var subscribed = _observable.Subscribe(result => { valueReturned = result; }); var item1 = new ObjectWithObservable(1); var item2 = new ObjectWithObservable(2); @@ -65,38 +77,42 @@ public void MultipleValuesReturnTrue() _source.AddOrUpdate(item1); _source.AddOrUpdate(item2); _source.AddOrUpdate(item3); - valuereturned.Value.Should().Be(false, "Value should be false"); + + if (valueReturned is null) + { + throw new InvalidOperationException(nameof(valueReturned)); + } + + valueReturned.Value.Should().Be(false, "Value should be false"); item1.InvokeObservable(true); item2.InvokeObservable(true); item3.InvokeObservable(true); - valuereturned.Value.Should().Be(true, "Value should be true"); + valueReturned.Value.Should().Be(true, "Value should be true"); subscribed.Dispose(); } private class ObjectWithObservable { - private readonly int _id; private readonly ISubject _changed = new Subject(); - private bool _value; public ObjectWithObservable(int id) { - _id = id; + Id = id; } + public int Id { get; } + + public IObservable Observable => _changed; + + public bool Value { get; private set; } + public void InvokeObservable(bool value) { - _value = value; + Value = value; _changed.OnNext(value); } - - public IObservable Observable { get { return _changed; } } - - public bool Value { get { return _value; } } - - public int Id { get { return _id; } } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/TrueForAnyFixture.cs b/src/DynamicData.Tests/Cache/TrueForAnyFixture.cs index 130d74d0d..6722d9ede 100644 --- a/src/DynamicData.Tests/Cache/TrueForAnyFixture.cs +++ b/src/DynamicData.Tests/Cache/TrueForAnyFixture.cs @@ -1,18 +1,20 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class TrueForAnyFixture: IDisposable + public class TrueForAnyFixture : IDisposable { - private readonly ISourceCache _source; private readonly IObservable _observable; - public TrueForAnyFixture() + private readonly ISourceCache _source; + + public TrueForAnyFixture() { _source = new SourceCache(p => p.Id); _observable = _source.Connect().TrueForAny(o => o.Observable.StartWith(o.Value), o => o == true); @@ -26,14 +28,14 @@ public void Dispose() [Fact] public void InitialItemReturnsFalseWhenObservaleHasNoValue() { - bool? valuereturned = null; - var subscribed = _observable.Subscribe(result => { valuereturned = result; }); + bool? valueReturned = null; + var subscribed = _observable.Subscribe(result => { valueReturned = result; }); var item = new ObjectWithObservable(1); _source.AddOrUpdate(item); - valuereturned.HasValue.Should().BeTrue(); - valuereturned.Value.Should().Be(false, "The intial value should be false"); + valueReturned.HasValue.Should().BeTrue(); + valueReturned!.Value.Should().Be(false, "The intial value should be false"); subscribed.Dispose(); } @@ -41,22 +43,23 @@ public void InitialItemReturnsFalseWhenObservaleHasNoValue() [Fact] public void InlineObservableChangeProducesResult() { - bool? valuereturned = null; - var subscribed = _observable.Subscribe(result => { valuereturned = result; }); + bool? valueReturned = null; + var subscribed = _observable.Subscribe(result => { valueReturned = result; }); var item = new ObjectWithObservable(1); item.InvokeObservable(true); _source.AddOrUpdate(item); - valuereturned.Value.Should().Be(true, "Value should be true"); + valueReturned.HasValue.Should().BeTrue(); + valueReturned!.Value.Should().Be(true, "Value should be true"); subscribed.Dispose(); } [Fact] public void MultipleValuesReturnTrue() { - bool? valuereturned = null; - var subscribed = _observable.Subscribe(result => { valuereturned = result; }); + bool? valueReturned = null; + var subscribed = _observable.Subscribe(result => { valueReturned = result; }); var item1 = new ObjectWithObservable(1); var item2 = new ObjectWithObservable(2); @@ -64,35 +67,39 @@ public void MultipleValuesReturnTrue() _source.AddOrUpdate(item1); _source.AddOrUpdate(item2); _source.AddOrUpdate(item3); - valuereturned.Value.Should().Be(false, "Value should be false"); + + if (valueReturned is null) + { + throw new InvalidOperationException(nameof(valueReturned)); + } + + valueReturned.Value.Should().Be(false, "Value should be false"); item1.InvokeObservable(true); - valuereturned.Value.Should().Be(true, "Value should be true"); + valueReturned.Value.Should().Be(true, "Value should be true"); subscribed.Dispose(); } private class ObjectWithObservable { - private readonly int _id; private readonly ISubject _changed = new Subject(); - private bool _value; public ObjectWithObservable(int id) { - _id = id; + Id = id; } + public int Id { get; } + + public IObservable Observable => _changed; + + public bool Value { get; private set; } + public void InvokeObservable(bool value) { - _value = value; + Value = value; _changed.OnNext(value); } - - public IObservable Observable { get { return _changed; } } - - public bool Value { get { return _value; } } - - public int Id { get { return _id; } } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/WatchFixture.cs b/src/DynamicData.Tests/Cache/WatchFixture.cs index 170e97362..946a22de1 100644 --- a/src/DynamicData.Tests/Cache/WatchFixture.cs +++ b/src/DynamicData.Tests/Cache/WatchFixture.cs @@ -1,38 +1,34 @@ using System; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Cache { - - public class WatchFixture: IDisposable + public class WatchFixture : IDisposable { - private class DisposableObject : IDisposable - { - public bool IsDisposed { get; private set; } - public int Id { get; private set; } - - public DisposableObject(int id) - { - Id = id; - } - - public void Dispose() - { - IsDisposed = true; - } - } + private readonly ChangeSetAggregator _results; private readonly ISourceCache _source; - private readonly ChangeSetAggregator _results; - public WatchFixture() + public WatchFixture() { _source = new SourceCache(p => p.Id); _results = new ChangeSetAggregator(_source.Connect().DisposeMany()); } + [Fact] + public void AddWillNotCallDispose() + { + _source.AddOrUpdate(new DisposableObject(1)); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Items.First().IsDisposed.Should().Be(false, "Should not be disposed"); + } + public void Dispose() { _source.Dispose(); @@ -40,13 +36,13 @@ public void Dispose() } [Fact] - public void AddWillNotCallDispose() + public void EverythingIsDisposedWhenStreamIsDisposed() { - _source.AddOrUpdate(new DisposableObject(1)); + _source.AddOrUpdate(Enumerable.Range(1, 10).Select(i => new DisposableObject(i))); + _source.Clear(); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - _results.Data.Items.First().IsDisposed.Should().Be(false, "Should not be disposed"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[1].All(d => d.Current.IsDisposed).Should().BeTrue(); } [Fact] @@ -72,14 +68,21 @@ public void UpdateWillCallDispose() _results.Messages[1].First().Previous.Value.IsDisposed.Should().Be(true, "Previous should be disposed"); } - [Fact] - public void EverythingIsDisposedWhenStreamIsDisposed() + private class DisposableObject : IDisposable { - _source.AddOrUpdate(Enumerable.Range(1, 10).Select(i => new DisposableObject(i))); - _source.Clear(); + public DisposableObject(int id) + { + Id = id; + } - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[1].All(d => d.Current.IsDisposed).Should().BeTrue(); + public int Id { get; private set; } + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/WatcherFixture.cs b/src/DynamicData.Tests/Cache/WatcherFixture.cs index bd186e0d8..0dd6ef08f 100644 --- a/src/DynamicData.Tests/Cache/WatcherFixture.cs +++ b/src/DynamicData.Tests/Cache/WatcherFixture.cs @@ -5,50 +5,47 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Experimental; using DynamicData.Tests.Domain; + +using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; -using FluentAssertions; #endregion namespace DynamicData.Tests.Cache { - - public class WatcherFixture: IDisposable + public class WatcherFixture : IDisposable { - private readonly TestScheduler _scheduler = new TestScheduler(); - private readonly ISourceCache _source; + private readonly IDisposable _cleanUp; + private readonly ChangeSetAggregator _results; - private readonly IWatcher _watcher; - private readonly IDisposable _cleanUp; + private readonly TestScheduler _scheduler = new(); + + private readonly ISourceCache _source; + + private readonly IWatcher _watcher; - public WatcherFixture() + public WatcherFixture() { _scheduler = new TestScheduler(); _source = new SourceCache(p => p.Key); _watcher = _source.Connect().AsWatcher(_scheduler); - _results = new ChangeSetAggregator - ( - _source.Connect() - .Transform(p => new SelfObservingPerson(_watcher.Watch(p.Key).Select(w => w.Current))) - .DisposeMany() - ); - - _cleanUp = Disposable.Create(() => - { - _results.Dispose(); - _source.Dispose(); - _watcher.Dispose(); - }); - } + _results = new ChangeSetAggregator(_source.Connect().Transform(p => new SelfObservingPerson(_watcher.Watch(p.Key).Select(w => w.Current))).DisposeMany()); - public void Dispose() - { - _cleanUp.Dispose(); + _cleanUp = Disposable.Create( + () => + { + _results.Dispose(); + _source.Dispose(); + _watcher.Dispose(); + }); } [Fact] @@ -65,23 +62,9 @@ public void AddNew() result.Completed.Should().Be(false, "Person should have received 1 update"); } - [Fact] - public void Update() + public void Dispose() { - var first = new Person("Adult1", 50); - var second = new Person("Adult1", 51); - _source.AddOrUpdate(first); - - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - _source.AddOrUpdate(second); - - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - _results.Messages.Count.Should().Be(2, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - - var secondResult = _results.Messages[1].First(); - secondResult.Previous.Value.UpdateCount.Should().Be(1, "Second Person should have received 1 update"); - secondResult.Previous.Value.Completed.Should().Be(true, "Second person should have received 1 update"); + _cleanUp.Dispose(); } [Fact] @@ -102,6 +85,25 @@ public void Remove() secondResult.Current.Completed.Should().Be(true, "Second person should have received 1 update"); } + [Fact] + public void Update() + { + var first = new Person("Adult1", 50); + var second = new Person("Adult1", 51); + _source.AddOrUpdate(first); + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + _source.AddOrUpdate(second); + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + _results.Messages.Count.Should().Be(2, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + + var secondResult = _results.Messages[1].First(); + secondResult.Previous.Value.UpdateCount.Should().Be(1, "Second Person should have received 1 update"); + secondResult.Previous.Value.Completed.Should().Be(true, "Second person should have received 1 update"); + } + [Fact] public void Watch() { @@ -165,4 +167,4 @@ public void WatchMany() watch3.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Cache/XorFixture.cs b/src/DynamicData.Tests/Cache/XorFixture.cs index e3185387a..b0870b0a4 100644 --- a/src/DynamicData.Tests/Cache/XorFixture.cs +++ b/src/DynamicData.Tests/Cache/XorFixture.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; + using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.Cache { - public class XOrFixture : XOrFixtureBase { protected override IObservable> CreateObservable() @@ -15,7 +17,7 @@ protected override IObservable> CreateObservable() } } - public class XOrCollectionFixture : XOrFixtureBase + public class XOrCollectionFixture : XOrFixtureBase { protected override IObservable> CreateObservable() { @@ -24,10 +26,12 @@ protected override IObservable> CreateObservable() } } - public abstract class XOrFixtureBase: IDisposable + public abstract class XOrFixtureBase : IDisposable { protected ISourceCache _source1; + protected ISourceCache _source2; + private readonly ChangeSetAggregator _results; protected XOrFixtureBase() @@ -37,8 +41,6 @@ protected XOrFixtureBase() _results = CreateObservable().AsAggregator(); } - protected abstract IObservable> CreateObservable(); - public void Dispose() { _source1.Dispose(); @@ -47,13 +49,15 @@ public void Dispose() } [Fact] - public void UpdatingOneSourceOnlyProducesResult() + public void RemovingFromOneDoesNotFromResult() { var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); + _source2.AddOrUpdate(person); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _source2.Remove(person); + _results.Messages.Count.Should().Be(3, "Should be 2 updates"); + _results.Data.Count.Should().Be(1, "Cache should have no items"); } [Fact] @@ -66,28 +70,28 @@ public void UpdatingBothDoeNotProducesResult() } [Fact] - public void RemovingFromOneDoesNotFromResult() + public void UpdatingOneProducesOnlyOneUpdate() { var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); _source2.AddOrUpdate(person); - _source2.Remove(person); - _results.Messages.Count.Should().Be(3, "Should be 2 updates"); - _results.Data.Count.Should().Be(1, "Cache should have no items"); + var personUpdated = new Person("Adult1", 51); + _source2.AddOrUpdate(personUpdated); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Data.Count.Should().Be(0, "Cache should have no items"); } [Fact] - public void UpdatingOneProducesOnlyOneUpdate() + public void UpdatingOneSourceOnlyProducesResult() { var person = new Person("Adult1", 50); _source1.AddOrUpdate(person); - _source2.AddOrUpdate(person); - var personUpdated = new Person("Adult1", 51); - _source2.AddOrUpdate(personUpdated); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Data.Count.Should().Be(0, "Cache should have no items"); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); } + + protected abstract IObservable> CreateObservable(); } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/Animal.cs b/src/DynamicData.Tests/Domain/Animal.cs index 462aaf16d..6f4113cd4 100644 --- a/src/DynamicData.Tests/Domain/Animal.cs +++ b/src/DynamicData.Tests/Domain/Animal.cs @@ -1,29 +1,23 @@ - -using DynamicData.Binding; +using DynamicData.Binding; namespace DynamicData.Tests.Domain { public enum AnimalFamily { Mammal, + Reptile, + Fish, + Amphibian, + Bird } public class Animal : AbstractNotifyPropertyChanged { - public string Name { get; } - public string Type { get; } - public AnimalFamily Family { get; } - private bool _includeInResults; - public bool IncludeInResults - { - get => _includeInResults; - set => SetAndRaise(ref _includeInResults, value); - } public Animal(string name, string type, AnimalFamily family) { @@ -31,5 +25,17 @@ public Animal(string name, string type, AnimalFamily family) Type = type; Family = family; } + + public AnimalFamily Family { get; } + + public bool IncludeInResults + { + get => _includeInResults; + set => SetAndRaise(ref _includeInResults, value); + } + + public string Name { get; } + + public string Type { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/ParentAndChildren.cs b/src/DynamicData.Tests/Domain/ParentAndChildren.cs index dad96b868..e5a3917e0 100644 --- a/src/DynamicData.Tests/Domain/ParentAndChildren.cs +++ b/src/DynamicData.Tests/Domain/ParentAndChildren.cs @@ -1,16 +1,11 @@ using System; + using DynamicData.Kernel; namespace DynamicData.Tests.Domain { public class ParentAndChildren : IEquatable { - public Person Parent { get; } - public string ParentId { get; } - public Person[] Children { get; } - - public int Count => Children.Length; - public ParentAndChildren(Person parent, Person[] children) { Parent = parent; @@ -19,14 +14,30 @@ public ParentAndChildren(Person parent, Person[] children) public ParentAndChildren(string parentId, Optional parent, Person[] children) { - Parent = parent.ValueOr(()=>null); + Parent = parent.ValueOrDefault(); ParentId = parentId; Children = children; } - #region Equality + public Person[] Children { get; } + + public int Count => Children.Length; + + public Person? Parent { get; } + + public string? ParentId { get; } + + public static bool operator ==(ParentAndChildren left, ParentAndChildren right) + { + return Equals(left, right); + } - public bool Equals(ParentAndChildren other) + public static bool operator !=(ParentAndChildren left, ParentAndChildren right) + { + return !Equals(left, right); + } + + public bool Equals(ParentAndChildren? other) { if (ReferenceEquals(null, other)) { @@ -41,7 +52,7 @@ public bool Equals(ParentAndChildren other) return string.Equals(ParentId, other.ParentId); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -58,26 +69,14 @@ public override bool Equals(object obj) return false; } - return Equals((ParentAndChildren) obj); + return Equals((ParentAndChildren)obj); } public override int GetHashCode() { - return (ParentId != null ? ParentId.GetHashCode() : 0); + return (ParentId is not null ? ParentId.GetHashCode() : 0); } - public static bool operator ==(ParentAndChildren left, ParentAndChildren right) - { - return Equals(left, right); - } - - public static bool operator !=(ParentAndChildren left, ParentAndChildren right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{nameof(Parent)}: {Parent}, ({Count} children)"; diff --git a/src/DynamicData.Tests/Domain/ParentChild.cs b/src/DynamicData.Tests/Domain/ParentChild.cs index d151daf0e..0df53cf33 100644 --- a/src/DynamicData.Tests/Domain/ParentChild.cs +++ b/src/DynamicData.Tests/Domain/ParentChild.cs @@ -2,13 +2,14 @@ { public class ParentChild { - public Person Child { get; } - public Person Parent { get; } - public ParentChild(Person child, Person parent) { Child = child; Parent = parent; } + + public Person Child { get; } + + public Person Parent { get; } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/Person.cs b/src/DynamicData.Tests/Domain/Person.cs index 8c5558dcf..84fd170f3 100644 --- a/src/DynamicData.Tests/Domain/Person.cs +++ b/src/DynamicData.Tests/Domain/Person.cs @@ -1,23 +1,25 @@ using System; using System.Collections.Generic; + using DynamicData.Binding; namespace DynamicData.Tests.Domain { public class Person : AbstractNotifyPropertyChanged, IEquatable { - public string ParentName { get; } - public string Name { get; } - public string Gender { get; } - public string Key => Name; private int _age; - public Person(string firstname, string lastname, int age, string gender = "F", string parentName = null) + public Person() + : this("unknown", 0, "none") + { + + } + public Person(string firstname, string lastname, int age, string gender = "F", string? parentName = null) : this(firstname + " " + lastname, age, gender, parentName) { } - public Person(string name, int age, string gender = "F", string parentName = null) + public Person(string name, int age, string gender = "F", string? parentName = null) { Name = name; _age = age; @@ -25,15 +27,35 @@ public Person(string name, int age, string gender = "F", string parentName = nul ParentName = parentName ?? string.Empty; } + public static IEqualityComparer AgeComparer { get; } = new AgeEqualityComparer(); + + public static IEqualityComparer NameAgeGenderComparer { get; } = new NameAgeGenderEqualityComparer(); + public int Age { get => _age; set => SetAndRaise(ref _age, value); } - #region Equality Members + public string Gender { get; } + + public string Key => Name; + + public string Name { get; } + + public string ParentName { get; } + + public static bool operator ==(Person left, Person right) + { + return Equals(left, right); + } + + public static bool operator !=(Person left, Person right) + { + return !Equals(left, right); + } - public bool Equals(Person other) + public bool Equals(Person? other) { if (ReferenceEquals(null, other)) { @@ -48,7 +70,7 @@ public bool Equals(Person other) return string.Equals(Name, other.Name); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -70,22 +92,17 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Name != null ? Name.GetHashCode() : 0); - } - - public static bool operator ==(Person left, Person right) - { - return Equals(left, right); + return (Name is not null ? Name.GetHashCode() : 0); } - public static bool operator !=(Person left, Person right) + public override string ToString() { - return !Equals(left, right); + return $"{Name}. {Age}"; } private sealed class AgeEqualityComparer : IEqualityComparer { - public bool Equals(Person x, Person y) + public bool Equals(Person? x, Person? y) { if (ReferenceEquals(x, y)) { @@ -116,11 +133,9 @@ public int GetHashCode(Person obj) } } - public static IEqualityComparer AgeComparer { get; } = new AgeEqualityComparer(); - private sealed class NameAgeGenderEqualityComparer : IEqualityComparer { - public bool Equals(Person x, Person y) + public bool Equals(Person? x, Person? y) { if (ReferenceEquals(x, y)) { @@ -149,21 +164,12 @@ public int GetHashCode(Person obj) { unchecked { - var hashCode = (obj.Name != null ? obj.Name.GetHashCode() : 0); + var hashCode = obj.Name.GetHashCode(); hashCode = (hashCode * 397) ^ obj._age; - hashCode = (hashCode * 397) ^ (obj.Gender != null ? obj.Gender.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ obj.Gender.GetHashCode(); return hashCode; } } } - - public static IEqualityComparer NameAgeGenderComparer { get; } = new NameAgeGenderEqualityComparer(); - - #endregion - - public override string ToString() - { - return $"{Name}. {Age}"; - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/PersonEmployment.cs b/src/DynamicData.Tests/Domain/PersonEmployment.cs index b9a7447d4..d4fb9c012 100644 --- a/src/DynamicData.Tests/Domain/PersonEmployment.cs +++ b/src/DynamicData.Tests/Domain/PersonEmployment.cs @@ -3,6 +3,7 @@ namespace DynamicData.Tests.Domain public struct PersonEmpKey { private readonly string _name; + private readonly string _company; public PersonEmpKey(string name, string company) @@ -22,7 +23,7 @@ public bool Equals(PersonEmpKey other) return string.Equals(_name, other._name) && string.Equals(_company, other._company); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -36,36 +37,27 @@ public override int GetHashCode() { unchecked { - return ((_name != null ? _name.GetHashCode() : 0) * 397) ^ (_company != null ? _company.GetHashCode() : 0); + return ((_name is not null ? _name.GetHashCode() : 0) * 397) ^ (_company is not null ? _company.GetHashCode() : 0); } } } public class PersonEmployment : IKey { - private readonly string _name; - private readonly string _company; - private readonly PersonEmpKey _key; - public PersonEmployment(string name, string company) { - _name = name; - _company = company; - _key = new PersonEmpKey(this); + Name = name; + Company = company; + Key = new PersonEmpKey(this); } - public string Name => _name; + public string Company { get; } - public string Company => _company; + public PersonEmpKey Key { get; } - public PersonEmpKey Key => _key; + public string Name { get; } - protected bool Equals(PersonEmployment other) - { - return string.Equals(_name, other._name) && string.Equals(_company, other._company); - } - - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -89,13 +81,18 @@ public override int GetHashCode() { unchecked { - return ((_name != null ? _name.GetHashCode() : 0) * 397) ^ (_company != null ? _company.GetHashCode() : 0); + return ((Name is not null ? Name.GetHashCode() : 0) * 397) ^ (Company is not null ? Company.GetHashCode() : 0); } } public override string ToString() { - return string.Format("Name: {0}, Company: {1}", _name, _company); + return string.Format("Name: {0}, Company: {1}", Name, Company); + } + + protected bool Equals(PersonEmployment other) + { + return string.Equals(Name, other.Name) && string.Equals(Company, other.Company); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/PersonObs.cs b/src/DynamicData.Tests/Domain/PersonObs.cs index f000905a0..22a2d6d23 100644 --- a/src/DynamicData.Tests/Domain/PersonObs.cs +++ b/src/DynamicData.Tests/Domain/PersonObs.cs @@ -1,25 +1,19 @@ using System; using System.Collections.Generic; using System.Reactive.Subjects; -using DynamicData.Annotations; namespace DynamicData.Tests.Domain { public class PersonObs : IEquatable { - public string ParentName { get; } - public string Name { get; } - public string Gender { get; } - public string Key => Name; - [NotNull] private readonly BehaviorSubject _age; - public PersonObs(string firstname, string lastname, int age, string gender = "F", string parentName = null) + public PersonObs(string firstname, string lastname, int age, string gender = "F", string? parentName = null) : this(firstname + " " + lastname, age, gender, parentName) { } - public PersonObs(string name, int age, string gender = "F", string parentName = null) + public PersonObs(string name, int age, string gender = "F", string? parentName = null) { Name = name; _age = new BehaviorSubject(age); @@ -27,16 +21,31 @@ public PersonObs(string name, int age, string gender = "F", string parentName = ParentName = parentName ?? string.Empty; } + public static IEqualityComparer AgeComparer { get; } = new AgeEqualityComparer(); + + public static IEqualityComparer NameAgeGenderComparer { get; } = new NameAgeGenderEqualityComparer(); + public IObservable Age => _age; - public void SetAge(int age) + public string Gender { get; } + + public string Key => Name; + + public string Name { get; } + + public string ParentName { get; } + + public static bool operator ==(PersonObs left, PersonObs right) { - _age.OnNext(age); + return Equals(left, right); } - #region Equality Members + public static bool operator !=(PersonObs left, PersonObs right) + { + return !Equals(left, right); + } - public bool Equals(PersonObs other) + public bool Equals(PersonObs? other) { if (ReferenceEquals(null, other)) { @@ -51,7 +60,7 @@ public bool Equals(PersonObs other) return string.Equals(Name, other.Name); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -73,22 +82,22 @@ public override bool Equals(object obj) public override int GetHashCode() { - return (Name != null ? Name.GetHashCode() : 0); + return (Name is not null ? Name.GetHashCode() : 0); } - public static bool operator ==(PersonObs left, PersonObs right) + public void SetAge(int age) { - return Equals(left, right); + _age.OnNext(age); } - public static bool operator !=(PersonObs left, PersonObs right) + public override string ToString() { - return !Equals(left, right); + return $"{Name}. {_age.Value}"; } private sealed class AgeEqualityComparer : IEqualityComparer { - public bool Equals(PersonObs x, PersonObs y) + public bool Equals(PersonObs? x, PersonObs? y) { if (ReferenceEquals(x, y)) { @@ -119,11 +128,9 @@ public int GetHashCode(PersonObs obj) } } - public static IEqualityComparer AgeComparer { get; } = new AgeEqualityComparer(); - private sealed class NameAgeGenderEqualityComparer : IEqualityComparer { - public bool Equals(PersonObs x, PersonObs y) + public bool Equals(PersonObs? x, PersonObs? y) { if (ReferenceEquals(x, y)) { @@ -152,21 +159,12 @@ public int GetHashCode(PersonObs obj) { unchecked { - var hashCode = (obj.Name != null ? obj.Name.GetHashCode() : 0); + var hashCode = (obj.Name is not null ? obj.Name.GetHashCode() : 0); hashCode = (hashCode * 397) ^ obj._age.Value; - hashCode = (hashCode * 397) ^ (obj.Gender != null ? obj.Gender.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (obj.Gender is not null ? obj.Gender.GetHashCode() : 0); return hashCode; } } } - - public static IEqualityComparer NameAgeGenderComparer { get; } = new NameAgeGenderEqualityComparer(); - - #endregion - - public override string ToString() - { - return $"{Name}. {_age.Value}"; - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/PersonWithChildren.cs b/src/DynamicData.Tests/Domain/PersonWithChildren.cs index 5a8728832..35bc65e02 100644 --- a/src/DynamicData.Tests/Domain/PersonWithChildren.cs +++ b/src/DynamicData.Tests/Domain/PersonWithChildren.cs @@ -5,9 +5,6 @@ namespace DynamicData.Tests.Domain { public class PersonWithChildren : IKey { - private readonly string _key; - private readonly string _name; - public PersonWithChildren(string name, int age) : this(name, age, Enumerable.Empty()) { @@ -15,33 +12,29 @@ public PersonWithChildren(string name, int age) public PersonWithChildren(string name, int age, IEnumerable relations) { - _name = name; + Name = name; Age = age; KeyValue = Name; Relations = relations; - _key = name; + Key = name; } - public string Name => _name; - public int Age { get; set; } + /// + /// The key + /// + public string Key { get; } + public string KeyValue { get; } + public string Name { get; } + public IEnumerable Relations { get; } public override string ToString() { return $"{Name}. {Age}"; } - - #region Implementation of IKey - - /// - /// The key - /// - public string Key { get { return _key; } } - - #endregion } } \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/PersonWithEmployment.cs b/src/DynamicData.Tests/Domain/PersonWithEmployment.cs index de816de5c..39627cb11 100644 --- a/src/DynamicData.Tests/Domain/PersonWithEmployment.cs +++ b/src/DynamicData.Tests/Domain/PersonWithEmployment.cs @@ -12,11 +12,11 @@ public PersonWithEmployment(IGroup sourc EmploymentData = source.Cache; } - public string Person => _source.Key; + public int EmploymentCount => EmploymentData.Count; public IObservableCache EmploymentData { get; } - public int EmploymentCount => EmploymentData.Count; + public string Person => _source.Key; public void Dispose() { @@ -28,4 +28,4 @@ public override string ToString() return $"Person: {Person}. Count {EmploymentCount}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/PersonWithFriends.cs b/src/DynamicData.Tests/Domain/PersonWithFriends.cs index 628ba0fee..fef14a3c8 100644 --- a/src/DynamicData.Tests/Domain/PersonWithFriends.cs +++ b/src/DynamicData.Tests/Domain/PersonWithFriends.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; + using DynamicData.Binding; namespace DynamicData.Tests.Domain @@ -7,6 +8,7 @@ namespace DynamicData.Tests.Domain public class PersonWithFriends : AbstractNotifyPropertyChanged, IKey { private int _age; + private IEnumerable _friends; public PersonWithFriends(string name, int age) @@ -22,8 +24,6 @@ public PersonWithFriends(string name, int age, IEnumerable fr Key = name; } - public string Name { get; } - public int Age { get => _age; @@ -36,18 +36,16 @@ public IEnumerable Friends set => SetAndRaise(ref _friends, value); } - public override string ToString() - { - return $"{Name}. {Age}"; - } - - #region Implementation of IKey - /// /// The key /// public string Key { get; } - #endregion + public string Name { get; } + + public override string ToString() + { + return $"{Name}. {Age}"; + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/PersonWithGender.cs b/src/DynamicData.Tests/Domain/PersonWithGender.cs index 2b4135c59..307ece440 100644 --- a/src/DynamicData.Tests/Domain/PersonWithGender.cs +++ b/src/DynamicData.Tests/Domain/PersonWithGender.cs @@ -4,33 +4,27 @@ namespace DynamicData.Tests.Domain { public class PersonWithGender : IEquatable { - private readonly string _name; - private readonly int _age; - private readonly string _gender; - public PersonWithGender(Person person, string gender) { - _name = person.Name; - _age = person.Age; - _gender = gender; + Name = person.Name; + Age = person.Age; + Gender = gender; } - public string Name { get { return _name; } } - - public int Age { get { return _age; } } - - public string Gender { get { return _gender; } } - public PersonWithGender(string name, int age, string gender) { - _name = name; - _age = age; - _gender = gender; + Name = name; + Age = age; + Gender = gender; } - #region Equality Members + public int Age { get; } - public bool Equals(PersonWithGender other) + public string Gender { get; } + + public string Name { get; } + + public bool Equals(PersonWithGender? other) { if (ReferenceEquals(null, other)) { @@ -45,7 +39,7 @@ public bool Equals(PersonWithGender other) return Equals(other.Name, Name) && other.Age == Age && Equals(other.Gender, Gender); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -69,18 +63,16 @@ public override int GetHashCode() { unchecked { - int result = (Name != null ? Name.GetHashCode() : 0); + int result = (Name is not null ? Name.GetHashCode() : 0); result = (result * 397) ^ Age; - result = (result * 397) ^ (Gender != null ? Gender.GetHashCode() : 0); + result = (result * 397) ^ (Gender is not null ? Gender.GetHashCode() : 0); return result; } } - #endregion - public override string ToString() { return $"{this.Name}. {this.Age} ({Gender})"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/PersonWithRelations.cs b/src/DynamicData.Tests/Domain/PersonWithRelations.cs index 8ce36e236..fc45cd5dc 100644 --- a/src/DynamicData.Tests/Domain/PersonWithRelations.cs +++ b/src/DynamicData.Tests/Domain/PersonWithRelations.cs @@ -5,9 +5,6 @@ namespace DynamicData.Tests.Domain { public class PersonWithRelations : IKey { - private readonly string _key; - private readonly string _name; - public PersonWithRelations(string name, int age) : this(name, age, Enumerable.Empty()) { @@ -15,35 +12,32 @@ public PersonWithRelations(string name, int age) public PersonWithRelations(string name, int age, IEnumerable relations) { - _name = name; + Name = name; Age = age; KeyValue = Name; Relations = relations; - _key = name; + Key = name; + Pet = Enumerable.Empty(); } - public string Name => _name; - public int Age { get; } + /// + /// The key + /// + public string Key { get; } + public string KeyValue { get; } - public IEnumerable Relations { get; } + public string Name { get; } public IEnumerable Pet { get; set; } + public IEnumerable Relations { get; } + public override string ToString() { return $"{Name}. {Age}"; } - - #region Implementation of IKey - - /// - /// The key - /// - public string Key { get { return _key; } } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/Pet.cs b/src/DynamicData.Tests/Domain/Pet.cs index 65752c10d..8d2325608 100644 --- a/src/DynamicData.Tests/Domain/Pet.cs +++ b/src/DynamicData.Tests/Domain/Pet.cs @@ -1,9 +1,9 @@ - namespace DynamicData.Tests.Domain { public class Pet { - public string Name { get; set; } - public string Animal { get; set; } + public string? Animal { get; set; } + + public string? Name { get; set; } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/RandomPersonGenerator.cs b/src/DynamicData.Tests/Domain/RandomPersonGenerator.cs index 979814106..716e4059d 100644 --- a/src/DynamicData.Tests/Domain/RandomPersonGenerator.cs +++ b/src/DynamicData.Tests/Domain/RandomPersonGenerator.cs @@ -6,53 +6,44 @@ namespace DynamicData.Tests.Domain { public class RandomPersonGenerator { - private readonly IEnumerable _boys = new List() - { - "Sergio", "Daniel", "Carolina", "David", "Reina", "Saul", "Bernard", "Danny", - "Dimas", "Yuri", "Ivan", "Laura", "John", "Bob", "Charles", "Rupert", "William", - "Englebert", "Aaron", "Quasimodo", "Henry", "Edward", "Zak", - "Kai", "Dominguez", "Escobar", "Martin", "Crespo", "Xavier", "Lyons", "Stephens", "Aaron" - }; + private readonly IEnumerable _boys = new List + { + "Sergio", "Daniel", "Carolina", "David", "Reina", "Saul", "Bernard", "Danny", + "Dimas", "Yuri", "Ivan", "Laura", "John", "Bob", "Charles", "Rupert", "William", + "Englebert", "Aaron", "Quasimodo", "Henry", "Edward", "Zak", + "Kai", "Dominguez", "Escobar", "Martin", "Crespo", "Xavier", "Lyons", "Stephens", "Aaron" + }; - private readonly IEnumerable _girls = new List() - { - "Ruth", "Katy", "Patricia", "Nikki", "Zoe", "Esmerelda", "Fiona", "Amber", "Kirsty", "Zaira", - "Claire", "Isabel", "Esmerelda", "Nicola", "Lucy", "Louise", "Elizabeth", "Anne", "Rebecca", - "Rhian", "Beatrice" - }; + private readonly IEnumerable _girls = new List + { + "Ruth", "Katy", "Patricia", "Nikki", "Zoe", "Esmerelda", "Fiona", "Amber", "Kirsty", "Zaira", + "Claire", "Isabel", "Esmerelda", "Nicola", "Lucy", "Louise", "Elizabeth", "Anne", "Rebecca", + "Rhian", "Beatrice" + }; - private readonly IEnumerable _lastnames = new List() - { - "Johnson", "Williams", "Jones", "Brown", "David", "Miller", "Wilson", "Anderson", "Thomas", - "Jackson", "White", "Robinson", "Williams", "Jones", "Windor", "McQueen", "X", "Black", - "Green", "Chicken", "Partrige", "Broad", "Flintoff", "Root" - }; + private readonly IEnumerable _lastnames = new List + { + "Johnson", "Williams", "Jones", "Brown", "David", "Miller", "Wilson", "Anderson", "Thomas", + "Jackson", "White", "Robinson", "Williams", "Jones", "Windor", "McQueen", "X", "Black", + "Green", "Chicken", "Partrige", "Broad", "Flintoff", "Root" + }; - private readonly Random _random = new Random(); + private readonly Random _random = new(); public IEnumerable Take(int number = 10000) { - var girls = (from first in _girls - from second in _lastnames - from third in _lastnames - select new { First = first, Second = second, Third = third, Gender = "F" }); + var girls = (from first in _girls from second in _lastnames from third in _lastnames select new { First = first, Second = second, Third = third, Gender = "F" }); - var boys = (from first in _boys - from second in _lastnames - from third in _lastnames - select new { First = first, Second = second, Third = third, Gender = "M" }); + var boys = (from first in _boys from second in _lastnames from third in _lastnames select new { First = first, Second = second, Third = third, Gender = "M" }); var maxage = 100; - return girls.Union(boys).OrderBy(x => Guid.NewGuid()) - .Select - (x => - { - var lastname = x.Second == x.Third ? x.Second : string.Format("{0}-{1}", x.Second, x.Third); - var age = _random.Next(0, maxage); - return new Person(x.First, lastname, age, x.Gender); - } - ) - .Take(number).ToList(); + return girls.Union(boys).OrderBy(x => Guid.NewGuid()).Select( + x => + { + var lastname = x.Second == x.Third ? x.Second : string.Format("{0}-{1}", x.Second, x.Third); + var age = _random.Next(0, maxage); + return new Person(x.First, lastname, age, x.Gender); + }).Take(number).ToList(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Domain/SelfObservingPerson.cs b/src/DynamicData.Tests/Domain/SelfObservingPerson.cs index 42d022460..2a286143f 100644 --- a/src/DynamicData.Tests/Domain/SelfObservingPerson.cs +++ b/src/DynamicData.Tests/Domain/SelfObservingPerson.cs @@ -5,27 +5,23 @@ namespace DynamicData.Tests.Domain { public class SelfObservingPerson : IDisposable { - private bool _completed; - private int _updateCount; - private Person _person; private readonly IDisposable _cleanUp; public SelfObservingPerson(IObservable observable) { - _cleanUp = observable.Finally(() => _completed = true).Subscribe(p => - { - _person = p; - _updateCount++; - }); + _cleanUp = observable.Finally(() => Completed = true).Subscribe( + p => + { + Person = p; + UpdateCount++; + }); } - public Person Person { get { return _person; } } + public bool Completed { get; private set; } - public int UpdateCount { get { return _updateCount; } } + public Person? Person { get; private set; } - public bool Completed { get { return _completed; } } - - #region Overrides of IDisposable + public int UpdateCount { get; private set; } /// ///put here the code to dispose all managed and unmanaged resources @@ -34,7 +30,5 @@ public void Dispose() { _cleanUp.Dispose(); } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/DynamicData.Tests.csproj b/src/DynamicData.Tests/DynamicData.Tests.csproj index 5e5bb9408..09bafb991 100644 --- a/src/DynamicData.Tests/DynamicData.Tests.csproj +++ b/src/DynamicData.Tests/DynamicData.Tests.csproj @@ -1,7 +1,9 @@  - netcoreapp3.1;net462 + netcoreapp3.1;net5.0 $(NoWarn);CS0618;CA1801 + enable + latest @@ -9,16 +11,16 @@ - + - + - + all runtime; build; native; contentfiles; analyzers diff --git a/src/DynamicData.Tests/EnumerableExFixtures.cs b/src/DynamicData.Tests/EnumerableExFixtures.cs index 420cbba35..891cf7fb9 100644 --- a/src/DynamicData.Tests/EnumerableExFixtures.cs +++ b/src/DynamicData.Tests/EnumerableExFixtures.cs @@ -1,43 +1,45 @@ using System; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests { public class EnumerableExFixtures { - private readonly Person _person1 = new Person("One", 1); - private readonly Person _person2 = new Person("Two", 2); - private readonly Person _person3 = new Person("Three", 3); + private readonly Person _person1 = new("One", 1); + + private readonly Person _person2 = new("Two", 2); + + private readonly Person _person3 = new("Three", 3); [Fact] - public void CanConvertToObservableChangeSetList() + public void CanConvertToObservableChangeSetCache() { - var source = new[] {_person1, _person2, _person3}; - var changeSet = source.AsObservableChangeSet(x => x.Age) - .AsObservableCache(); + var source = new[] { _person1, _person2, _person3 }; + var changeSet = source.AsObservableChangeSet().AsObservableList(); changeSet.Items.Should().BeEquivalentTo(source); } [Fact] - public void CanConvertToObservableChangeSetCache() + public void CanConvertToObservableChangeSetList() { - var source = new[] {_person1, _person2, _person3}; - var changeSet = source.AsObservableChangeSet() - .AsObservableList(); + var source = new[] { _person1, _person2, _person3 }; + var changeSet = source.AsObservableChangeSet(x => x.Age).AsObservableCache(); changeSet.Items.Should().BeEquivalentTo(source); } [Theory] [InlineData(true)] [InlineData(false)] - public void RespectsCompleteConfigurationForList(bool shouldComplete) + public void RespectsCompleteConfigurationForCache(bool shouldComplete) { var completed = false; - var source = new[] {_person1, _person2, _person3}; - using (source.AsObservableChangeSet(shouldComplete) - .Subscribe(_ => { }, () => completed = true)) + var source = new[] { _person1, _person2, _person3 }; + using (source.AsObservableChangeSet(x => x.Age, shouldComplete).Subscribe(_ => { }, () => completed = true)) { Assert.Equal(completed, shouldComplete); } @@ -46,12 +48,11 @@ public void RespectsCompleteConfigurationForList(bool shouldComplete) [Theory] [InlineData(true)] [InlineData(false)] - public void RespectsCompleteConfigurationForCache(bool shouldComplete) + public void RespectsCompleteConfigurationForList(bool shouldComplete) { var completed = false; - var source = new[] {_person1, _person2, _person3}; - using (source.AsObservableChangeSet(x => x.Age, shouldComplete) - .Subscribe(_ => { }, () => completed = true)) + var source = new[] { _person1, _person2, _person3 }; + using (source.AsObservableChangeSet(shouldComplete).Subscribe(_ => { }, () => completed = true)) { Assert.Equal(completed, shouldComplete); } diff --git a/src/DynamicData.Tests/Kernal/CacheUpdaterFixture.cs b/src/DynamicData.Tests/Kernal/CacheUpdaterFixture.cs index fc7b9efa8..31910453c 100644 --- a/src/DynamicData.Tests/Kernal/CacheUpdaterFixture.cs +++ b/src/DynamicData.Tests/Kernal/CacheUpdaterFixture.cs @@ -1,19 +1,21 @@ - -using System.Linq; +using System.Linq; + using DynamicData.Cache.Internal; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Kernal { - public class CacheUpdaterFixture { private readonly ChangeAwareCache _cache; + private readonly CacheUpdater _updater; - public CacheUpdaterFixture() + public CacheUpdaterFixture() { _cache = new ChangeAwareCache(); _updater = new CacheUpdater(_cache); @@ -77,6 +79,5 @@ public void Update() 1.Should().Be(updates.Count(update => update.Reason == ChangeReason.Update), "Should be 1 update"); 2.Should().Be(updates.Count, "Should be 2 updates"); } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Kernal/DistinctUpdateFixture.cs b/src/DynamicData.Tests/Kernal/DistinctUpdateFixture.cs index 34ff71b0d..2af2a90cc 100644 --- a/src/DynamicData.Tests/Kernal/DistinctUpdateFixture.cs +++ b/src/DynamicData.Tests/Kernal/DistinctUpdateFixture.cs @@ -1,12 +1,14 @@ using System; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Kernal { - public class DistinctUpdateFixture { [Fact] @@ -55,4 +57,4 @@ public void UpdateWillThrowIfNoPreviousValueIsSupplied() Assert.Throws(() => new Change(ChangeReason.Update, current, current)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Kernal/EnumerableEx.cs b/src/DynamicData.Tests/Kernal/EnumerableEx.cs index 6e4b961cd..4a445daa7 100644 --- a/src/DynamicData.Tests/Kernal/EnumerableEx.cs +++ b/src/DynamicData.Tests/Kernal/EnumerableEx.cs @@ -5,83 +5,80 @@ namespace DynamicData.Tests.Cache { public static class EnumerableEx { - public static IEnumerable PrevCurrentNextZip(this IEnumerable source, - Func selector) + public static IEnumerable CurrentNextZip(this IEnumerable source, Func selector) { - if (source == null) + if (source is null) { - throw new ArgumentNullException("source"); + throw new ArgumentNullException(nameof(source)); } - if (selector == null) + if (selector is null) { - throw new ArgumentNullException("selector"); + throw new ArgumentNullException(nameof(selector)); } var enumerator = source.GetEnumerator(); if (enumerator.MoveNext()) { - T prev = default(T); T curr = enumerator.Current; while (enumerator.MoveNext()) { var next = enumerator.Current; - yield return selector(prev, curr, next); - prev = curr; + yield return selector(curr, next); curr = next; } - yield return selector(prev, curr, default(T)); + yield return selector(curr, default); } } - public static IEnumerable CurrentNextZip(this IEnumerable source, - Func selector) + public static IEnumerable PrevCurrentNextZip(this IEnumerable source, Func selector) { - if (source == null) + if (source is null) { - throw new ArgumentNullException("source"); + throw new ArgumentNullException(nameof(source)); } - if (selector == null) + if (selector is null) { - throw new ArgumentNullException("selector"); + throw new ArgumentNullException(nameof(selector)); } var enumerator = source.GetEnumerator(); if (enumerator.MoveNext()) { + T? prev = default(T); T curr = enumerator.Current; while (enumerator.MoveNext()) { var next = enumerator.Current; - yield return selector(curr, next); + yield return selector(prev, curr, next); + prev = curr; curr = next; } - yield return selector(curr, default(T)); + yield return selector(prev, curr, default); } } - public static IEnumerable PrevCurrentZip(this IEnumerable source, - Func selector) + public static IEnumerable PrevCurrentZip(this IEnumerable source, Func selector) { - if (source == null) + if (source is null) { - throw new ArgumentNullException("source"); + throw new ArgumentNullException(nameof(source)); } - if (selector == null) + if (selector is null) { - throw new ArgumentNullException("selector"); + throw new ArgumentNullException(nameof(selector)); } var enumerator = source.GetEnumerator(); if (enumerator.MoveNext()) { - T prev = default(T); + T? prev = default(T); T curr = enumerator.Current; while (enumerator.MoveNext()) @@ -94,4 +91,4 @@ public static IEnumerable PrevCurrentZip(this IEnumerable? source = null; - [Fact] - public void OptionSetToNullHasNoValue2() - { - Person person = null; - Optional option = person; - option.HasValue.Should().BeFalse(); - } + bool ifactioninvoked = false; + bool elseactioninvoked = false; - [Fact] - public void OptionNoneHasNoValue() - { - var option = Optional.None>(); - option.HasValue.Should().BeFalse(); + source.IfHasValue(p => ifactioninvoked = true).Else(() => elseactioninvoked = true); + + ifactioninvoked.Should().BeFalse(); + elseactioninvoked.Should().BeTrue(); } [Fact] @@ -58,26 +41,42 @@ public void OptionIfHasValueInvokedIfOptionHasValue() bool ifactioninvoked = false; bool elseactioninvoked = false; - source.IfHasValue(p => ifactioninvoked = true) - .Else(() => elseactioninvoked = true); + source.IfHasValue(p => ifactioninvoked = true).Else(() => elseactioninvoked = true); ifactioninvoked.Should().BeTrue(); elseactioninvoked.Should().BeFalse(); } [Fact] - public void OptionElseInvokedIfOptionHasNoValue() + public void OptionNoneHasNoValue() { - Optional source = null; + var option = Optional.None>(); + option.HasValue.Should().BeFalse(); + } - bool ifactioninvoked = false; - bool elseactioninvoked = false; + [Fact] + public void OptionSetToNullHasNoValue1() + { + Person? person = null; + var option = Optional.Some(person); + option.HasValue.Should().BeFalse(); + } - source.IfHasValue(p => ifactioninvoked = true) - .Else(() => elseactioninvoked = true); + [Fact] + public void OptionSetToNullHasNoValue2() + { + Person? person = null; + Optional option = person; + option.HasValue.Should().BeFalse(); + } - ifactioninvoked.Should().BeFalse(); - elseactioninvoked.Should().BeTrue(); + [Fact] + public void OptionSomeHasValue() + { + var person = new Person("Name", 20); + var option = Optional.Some(person); + option.HasValue.Should().BeTrue(); + ReferenceEquals(person, option.Value).Should().BeTrue(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Kernal/SourceUpdaterFixture.cs b/src/DynamicData.Tests/Kernal/SourceUpdaterFixture.cs index b713b8fcd..13ab55c31 100644 --- a/src/DynamicData.Tests/Kernal/SourceUpdaterFixture.cs +++ b/src/DynamicData.Tests/Kernal/SourceUpdaterFixture.cs @@ -1,7 +1,10 @@ using System.Linq; + using DynamicData.Cache.Internal; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Kernal @@ -9,9 +12,10 @@ namespace DynamicData.Tests.Kernal public class SourceUpdaterFixture { private readonly ChangeAwareCache _cache; + private readonly CacheUpdater _updater; - public SourceUpdaterFixture() + public SourceUpdaterFixture() { _cache = new ChangeAwareCache(); _updater = new CacheUpdater(_cache, p => p.Name); @@ -137,6 +141,5 @@ public void NullSelectorWillThrow() { // Assert.Throws(() => new SourceUpdater(_cache, new KeySelector(null))); } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Kernal/UpdateFixture.cs b/src/DynamicData.Tests/Kernal/UpdateFixture.cs index 3a1311aa4..5d1fb0c0f 100644 --- a/src/DynamicData.Tests/Kernal/UpdateFixture.cs +++ b/src/DynamicData.Tests/Kernal/UpdateFixture.cs @@ -1,12 +1,14 @@ using System; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.Kernal { - public class UpdateFixture { [Fact] @@ -54,4 +56,4 @@ public void UpdateWillThrowIfNoPreviousValueIsSupplied() Assert.Throws(() => new Change(ChangeReason.Update, "Person", current)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/AndFixture.cs b/src/DynamicData.Tests/List/AndFixture.cs index 250279bf4..4a466471e 100644 --- a/src/DynamicData.Tests/List/AndFixture.cs +++ b/src/DynamicData.Tests/List/AndFixture.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class AndFixture : AndFixtureBase { protected override IObservable> CreateObservable() @@ -24,10 +25,12 @@ protected override IObservable> CreateObservable() } } - public abstract class AndFixtureBase: IDisposable + public abstract class AndFixtureBase : IDisposable { protected ISourceList _source1; + protected ISourceList _source2; + private readonly ChangeSetAggregator _results; protected AndFixtureBase() @@ -37,7 +40,23 @@ protected AndFixtureBase() _results = CreateObservable().AsAggregator(); } - protected abstract IObservable> CreateObservable(); + [Fact] + public void ClearOneClearsResult() + { + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(1, 5)); + _source1.Clear(); + _results.Data.Count.Should().Be(0); + } + + [Fact] + public void CombineRange() + { + _source1.AddRange(Enumerable.Range(1, 10)); + _source2.AddRange(Enumerable.Range(6, 10)); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); + } public void Dispose() { @@ -70,33 +89,15 @@ public void RemovedWhenNoLongerInBoth() _results.Data.Count.Should().Be(0); } - [Fact] - public void CombineRange() - { - _source1.AddRange(Enumerable.Range(1, 10)); - _source2.AddRange(Enumerable.Range(6, 10)); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); - } - - [Fact] - public void ClearOneClearsResult() - { - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(1, 5)); - _source1.Clear(); - _results.Data.Count.Should().Be(0); - } - [Fact] public void StartingWithNonEmptySourceProducesNoResult() { _source1.Add(1); - using (var result = CreateObservable().AsAggregator()) - { - result.Data.Count.Should().Be(0); - } + using var result = CreateObservable().AsAggregator(); + result.Data.Count.Should().Be(0); } + + protected abstract IObservable> CreateObservable(); } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/AutoRefreshFixture.cs b/src/DynamicData.Tests/List/AutoRefreshFixture.cs index e6f6c5010..b66d7387b 100644 --- a/src/DynamicData.Tests/List/AutoRefreshFixture.cs +++ b/src/DynamicData.Tests/List/AutoRefreshFixture.cs @@ -1,55 +1,54 @@ using System; using System.Linq; + using DynamicData.Binding; using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.List { - public class AutoRefreshFixture { [Fact] public void AutoRefresh() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, 1)) - .ToArray(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, 1)).ToArray(); //result should only be true when all items are set to true - using (var list = new SourceList()) - using (var results = list.Connect().AutoRefresh(p=>p.Age).AsAggregator()) - { - list.AddRange(items); - - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(1); - - items[0].Age = 10; - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(2); - - results.Messages[1].First().Reason.Should().Be(ListChangeReason.Refresh); - - //remove an item and check no change is fired - var toRemove = items[1]; - list.Remove(toRemove); - results.Data.Count.Should().Be(99); - results.Messages.Count.Should().Be(3); - toRemove.Age = 100; - results.Messages.Count.Should().Be(3); - - //add it back in and check it updates - list.Add(toRemove); - results.Messages.Count.Should().Be(4); - toRemove.Age = 101; - results.Messages.Count.Should().Be(5); - - results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Refresh); - } + using var list = new SourceList(); + using var results = list.Connect().AutoRefresh(p => p.Age).AsAggregator(); + list.AddRange(items); + + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(1); + + items[0].Age = 10; + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(2); + + results.Messages[1].First().Reason.Should().Be(ListChangeReason.Refresh); + + //remove an item and check no change is fired + var toRemove = items[1]; + list.Remove(toRemove); + results.Data.Count.Should().Be(99); + results.Messages.Count.Should().Be(3); + toRemove.Age = 100; + results.Messages.Count.Should().Be(3); + + //add it back in and check it updates + list.Add(toRemove); + results.Messages.Count.Should().Be(4); + toRemove.Age = 101; + results.Messages.Count.Should().Be(5); + + results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Refresh); } [Fact] @@ -57,368 +56,328 @@ public void AutoRefreshBatched() { var scheduler = new TestScheduler(); - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, 1)) - .ToArray(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, 1)).ToArray(); //result should only be true when all items are set to true - using (var list = new SourceList()) - using (var results = list.Connect().AutoRefresh(p=>p.Age, TimeSpan.FromSeconds(1),scheduler: scheduler).AsAggregator()) - { - list.AddRange(items); + using var list = new SourceList(); + using var results = list.Connect().AutoRefresh(p => p.Age, TimeSpan.FromSeconds(1), scheduler: scheduler).AsAggregator(); + list.AddRange(items); - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(1); + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(1); - //update 50 records - items.Skip(50) - .ForEach(p => p.Age = p.Age + 1); + //update 50 records + items.Skip(50).ForEach(p => p.Age += 1); - scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); - //should be another message with 50 refreshes - results.Messages.Count.Should().Be(2); - results.Messages[1].Refreshes.Should().Be(50); - } + //should be another message with 50 refreshes + results.Messages.Count.Should().Be(2); + results.Messages[1].Refreshes.Should().Be(50); } [Fact] - public void AutoRefreshFilter() + public void AutoRefreshDistinct() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); //result should only be true when all items are set to true - using (var list = new SourceList()) - using (var results = list.Connect().AutoRefresh(p=>p.Age).Filter(p=>p.Age>50).AsAggregator()) - { - list.AddRange(items); - - results.Data.Count.Should().Be(50); - results.Messages.Count.Should().Be(1); - - //update an item which did not match the filter and does so after change - items[0].Age = 60; - results.Data.Count.Should().Be(51); - results.Messages.Count.Should().Be(2); - results.Messages[1].First().Reason.Should().Be(ListChangeReason.Add); - - //check for removes - items[0].Age = 21; - results.Data.Count.Should().Be(50); - results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Remove); - items[0].Age = 60; - - //update an item which matched the filter and still does [refresh should have propagated] - items[60].Age = 160; - results.Data.Count.Should().Be(51); - results.Messages.Count.Should().Be(5); - results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Replace); - - //remove an item and check no change is fired - var toRemove = items[65]; - list.Remove(toRemove); - results.Data.Count.Should().Be(50); - results.Messages.Count.Should().Be(6); - toRemove.Age = 100; - results.Messages.Count.Should().Be(6); - - //add it back in and check it updates - list.Add(toRemove); - results.Messages.Count.Should().Be(7); - toRemove.Age = 101; - results.Messages.Count.Should().Be(8); - - results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Replace); - } - } + using var list = new SourceList(); + using var results = list.Connect().AutoRefresh(p => p.Age).DistinctValues(p => p.Age / 10).AsAggregator(); + list.AddRange(items); - [Fact] - public void AutoRefreshTransform() - { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + results.Data.Count.Should().Be(11); + results.Messages.Count.Should().Be(1); - //result should only be true when all items are set to true - using (var list = new SourceList()) - using (var results = list.Connect() - .AutoRefresh(p=>p.Age) - .Transform((p,idx) => new TransformedPerson(p,idx)) - .AsAggregator()) - { - list.AddRange(items); - - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(1); - - //update an item which did not match the filter and does so after change - items[0].Age = 60; - results.Messages.Count.Should().Be(2); - results.Messages.Last().Refreshes.Should().Be(1); - results.Messages.Last().First().Item.Reason.Should().Be(ListChangeReason.Refresh); - results.Messages.Last().First().Item.Current.Index.Should().Be(0); - - items[60].Age = 160; - results.Messages.Count.Should().Be(3); - results.Messages.Last().Refreshes.Should().Be(1); - results.Messages.Last().First().Item.Reason.Should().Be(ListChangeReason.Refresh); - results.Messages.Last().First().Item.Current.Index.Should().Be(60); - } + //update an item which did not match the filter and does so after change + items[50].Age = 500; + results.Data.Count.Should().Be(12); + + results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Add); + results.Messages.Last().First().Item.Current.Should().Be(50); } [Fact] - public void AutoRefreshSort() + public void AutoRefreshFilter() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, i)) - .OrderByDescending(p=>p.Age) - .ToArray(); - - var comparer = SortExpressionComparer.Ascending(p => p.Age); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); //result should only be true when all items are set to true - using (var list = new SourceList()) - using (var results = list.Connect() - .AutoRefresh(p => p.Age) - .Sort(SortExpressionComparer.Ascending(p=>p.Age)) - .AsAggregator()) - { - - void CheckOrder() - { - var sorted = items.OrderBy(p => p, comparer).ToArray(); - results.Data.Items.Should().BeEquivalentTo(sorted); - } - - list.AddRange(items); - - results.Data.Count.Should().Be(100); - results.Messages.Count.Should().Be(1); - CheckOrder(); - - items[0].Age = 60; - CheckOrder(); - results.Messages.Count.Should().Be(2); - results.Messages.Last().Refreshes.Should().Be(1); - results.Messages.Last().Moves.Should().Be(1); - - items[90].Age = -1; //move to beginning - CheckOrder(); - results.Messages.Count.Should().Be(3); - results.Messages.Last().Refreshes.Should().Be(1); - results.Messages.Last().Moves.Should().Be(1); - - items[50].Age = 49; //same position so no move - CheckOrder(); - results.Messages.Count.Should().Be(4); - results.Messages.Last().Refreshes.Should().Be(1); - results.Messages.Last().Moves.Should().Be(0); - - items[50].Age = 51; //same position so no move - CheckOrder(); - results.Messages.Count.Should().Be(5); - results.Messages.Last().Refreshes.Should().Be(1); - results.Messages.Last().Moves.Should().Be(1); - } + using var list = new SourceList(); + using var results = list.Connect().AutoRefresh(p => p.Age).Filter(p => p.Age > 50).AsAggregator(); + list.AddRange(items); + + results.Data.Count.Should().Be(50); + results.Messages.Count.Should().Be(1); + + //update an item which did not match the filter and does so after change + items[0].Age = 60; + results.Data.Count.Should().Be(51); + results.Messages.Count.Should().Be(2); + results.Messages[1].First().Reason.Should().Be(ListChangeReason.Add); + + //check for removes + items[0].Age = 21; + results.Data.Count.Should().Be(50); + results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Remove); + items[0].Age = 60; + + //update an item which matched the filter and still does [refresh should have propagated] + items[60].Age = 160; + results.Data.Count.Should().Be(51); + results.Messages.Count.Should().Be(5); + results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Replace); + + //remove an item and check no change is fired + var toRemove = items[65]; + list.Remove(toRemove); + results.Data.Count.Should().Be(50); + results.Messages.Count.Should().Be(6); + toRemove.Age = 100; + results.Messages.Count.Should().Be(6); + + //add it back in and check it updates + list.Add(toRemove); + results.Messages.Count.Should().Be(7); + toRemove.Age = 101; + results.Messages.Count.Should().Be(8); + + results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Replace); } [Fact] public void AutoRefreshGroup() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); //result should only be true when all items are set to true - using (var list = new SourceList()) - using (var results = list.Connect() - .AutoRefresh(p => p.Age) - .GroupOn(p=>p.Age % 10) - .AsAggregator()) + using var list = new SourceList(); + using var results = list.Connect().AutoRefresh(p => p.Age).GroupOn(p => p.Age % 10).AsAggregator(); + void CheckContent() { - void CheckContent() + foreach (var grouping in items.GroupBy(p => p.Age % 10)) { - foreach (var grouping in items.GroupBy(p => p.Age % 10)) - { - var childGroup = results.Data.Items.Single(g => g.GroupKey == grouping.Key); - var expected = grouping.OrderBy(p => p.Name); - var actual = childGroup.List.Items.OrderBy(p => p.Name); - actual.Should().BeEquivalentTo(expected); - } + var childGroup = results.Data.Items.Single(g => g.GroupKey == grouping.Key); + var expected = grouping.OrderBy(p => p.Name); + var actual = childGroup.List.Items.OrderBy(p => p.Name); + actual.Should().BeEquivalentTo(expected); } - - list.AddRange(items); - results.Data.Count.Should().Be(10); - results.Messages.Count.Should().Be(1); - CheckContent(); - - //move person from group 1 to 2 - items[0].Age = items[0].Age + 1; - CheckContent(); - - //change the value and move to a grouping which does not yet exist - items[1].Age = -1; - results.Data.Count.Should().Be(11); - results.Data.Items.Last().GroupKey.Should().Be(-1); - results.Data.Items.Last().List.Count.Should().Be(1); - results.Data.Items.First().List.Count.Should().Be(9); - CheckContent(); - - //put the value back where it was and check the group was removed - items[1].Age = 1; - results.Data.Count.Should().Be(10); - CheckContent(); - - var groupOf3 = results.Data.Items.ElementAt(2); - - IChangeSet changes = null; - groupOf3.List.Connect().Subscribe(c => changes = c); - - //refresh an item which makes it belong to the same group - should then propagate a refresh - items[2].Age = 13; - changes.Should().NotBeNull(); - changes.Count.Should().Be(1); - changes.First().Reason.Should().Be(ListChangeReason.Replace); - changes.First().Item.Current.Should().BeSameAs(items[2]); } + + list.AddRange(items); + results.Data.Count.Should().Be(10); + results.Messages.Count.Should().Be(1); + CheckContent(); + + //move person from group 1 to 2 + items[0].Age = items[0].Age + 1; + CheckContent(); + + //change the value and move to a grouping which does not yet exist + items[1].Age = -1; + results.Data.Count.Should().Be(11); + results.Data.Items.Last().GroupKey.Should().Be(-1); + results.Data.Items.Last().List.Count.Should().Be(1); + results.Data.Items.First().List.Count.Should().Be(9); + CheckContent(); + + //put the value back where it was and check the group was removed + items[1].Age = 1; + results.Data.Count.Should().Be(10); + CheckContent(); + + var groupOf3 = results.Data.Items.ElementAt(2); + + IChangeSet? changes = null; + groupOf3.List.Connect().Subscribe(c => changes = c); + + //refresh an item which makes it belong to the same group - should then propagate a refresh + items[2].Age = 13; + changes.Should().NotBeNull(); + changes!.Count.Should().Be(1); + changes!.First().Reason.Should().Be(ListChangeReason.Replace); + changes!.First().Item.Current.Should().BeSameAs(items[2]); } [Fact] public void AutoRefreshGroupImmutable() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); //result should only be true when all items are set to true - using (var list = new SourceList()) - using (var results = list.Connect() - .AutoRefresh(p => p.Age) - .GroupWithImmutableState(p => p.Age % 10) - .AsAggregator()) + using var list = new SourceList(); + using var results = list.Connect().AutoRefresh(p => p.Age).GroupWithImmutableState(p => p.Age % 10).AsAggregator(); + void CheckContent() { - void CheckContent() + foreach (var grouping in items.GroupBy(p => p.Age % 10)) { - foreach (var grouping in items.GroupBy(p => p.Age % 10)) - { - var childGroup = results.Data.Items.Single(g => g.Key == grouping.Key); - var expected = grouping.OrderBy(p => p.Name); - var actual = childGroup.Items.OrderBy(p => p.Name); - actual.Should().BeEquivalentTo(expected); - } + var childGroup = results.Data.Items.Single(g => g.Key == grouping.Key); + var expected = grouping.OrderBy(p => p.Name); + var actual = childGroup.Items.OrderBy(p => p.Name); + actual.Should().BeEquivalentTo(expected); } - - list.AddRange(items); - results.Data.Count.Should().Be(10); - results.Messages.Count.Should().Be(1); - CheckContent(); - - //move person from group 1 to 2 - items[0].Age = items[0].Age + 1; - CheckContent(); - - //change the value and move to a grouping which does not yet exist - items[1].Age = -1; - results.Data.Count.Should().Be(11); - results.Data.Items.Last().Key.Should().Be(-1); - results.Data.Items.Last().Count.Should().Be(1); - results.Data.Items.First().Count.Should().Be(9); - CheckContent(); - - //put the value back where it was and check the group was removed - items[1].Age = 1; - results.Data.Count.Should().Be(10); - results.Messages.Count.Should().Be(4); - CheckContent(); - - //refresh an item which makes it belong to the same group - should then propagate a refresh - items[2].Age = 13; - CheckContent(); - - results.Messages.Count.Should().Be(5); } + + list.AddRange(items); + results.Data.Count.Should().Be(10); + results.Messages.Count.Should().Be(1); + CheckContent(); + + //move person from group 1 to 2 + items[0].Age = items[0].Age + 1; + CheckContent(); + + //change the value and move to a grouping which does not yet exist + items[1].Age = -1; + results.Data.Count.Should().Be(11); + results.Data.Items.Last().Key.Should().Be(-1); + results.Data.Items.Last().Count.Should().Be(1); + results.Data.Items.First().Count.Should().Be(9); + CheckContent(); + + //put the value back where it was and check the group was removed + items[1].Age = 1; + results.Data.Count.Should().Be(10); + results.Messages.Count.Should().Be(4); + CheckContent(); + + //refresh an item which makes it belong to the same group - should then propagate a refresh + items[2].Age = 13; + CheckContent(); + + results.Messages.Count.Should().Be(5); } [Fact] - public void AutoRefreshDistinct() + public void AutoRefreshSelected() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, i)) - .ToArray(); + //test added as v6 broke unit test in DynamicData.Snippets + var initialItems = Enumerable.Range(1, 10).Select(i => new SelectableItem(i)).ToArray(); //result should only be true when all items are set to true - using (var list = new SourceList()) - using (var results = list.Connect() - .AutoRefresh(p => p.Age) - .DistinctValues(p => p.Age / 10) - .AsAggregator()) - { - list.AddRange(items); + using var sourceList = new SourceList(); + using var sut = sourceList.Connect().AutoRefresh().Filter(si => si.IsSelected).AsObservableList(); + sourceList.AddRange(initialItems); + sut.Count.Should().Be(0); - results.Data.Count.Should().Be(11); - results.Messages.Count.Should().Be(1); + initialItems[0].IsSelected = true; + sut.Count.Should().Be(1); - //update an item which did not match the filter and does so after change - items[50].Age = 500; - results.Data.Count.Should().Be(12); + initialItems[1].IsSelected = true; + sut.Count.Should().Be(2); - results.Messages.Last().First().Reason.Should().Be(ListChangeReason.Add); - results.Messages.Last().First().Item.Current.Should().Be(50); - } + //remove the selected items + sourceList.RemoveRange(0, 2); + sut.Count.Should().Be(0); } - private class TransformedPerson + [Fact] + public void AutoRefreshSort() { - public Person Person { get; } - public int Index { get; } - public DateTime TimeStamp { get; } = DateTime.Now; + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).OrderByDescending(p => p.Age).ToArray(); - public TransformedPerson(Person person, int index) + var comparer = SortExpressionComparer.Ascending(p => p.Age); + + //result should only be true when all items are set to true + using var list = new SourceList(); + using var results = list.Connect().AutoRefresh(p => p.Age).Sort(SortExpressionComparer.Ascending(p => p.Age)).AsAggregator(); + void CheckOrder() { - Person = person; - Index = index; + var sorted = items.OrderBy(p => p, comparer).ToArray(); + results.Data.Items.Should().BeEquivalentTo(sorted); } + + list.AddRange(items); + + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(1); + CheckOrder(); + + items[0].Age = 60; + CheckOrder(); + results.Messages.Count.Should().Be(2); + results.Messages.Last().Refreshes.Should().Be(1); + results.Messages.Last().Moves.Should().Be(1); + + items[90].Age = -1; //move to beginning + CheckOrder(); + results.Messages.Count.Should().Be(3); + results.Messages.Last().Refreshes.Should().Be(1); + results.Messages.Last().Moves.Should().Be(1); + + items[50].Age = 49; //same position so no move + CheckOrder(); + results.Messages.Count.Should().Be(4); + results.Messages.Last().Refreshes.Should().Be(1); + results.Messages.Last().Moves.Should().Be(0); + + items[50].Age = 51; //same position so no move + CheckOrder(); + results.Messages.Count.Should().Be(5); + results.Messages.Last().Refreshes.Should().Be(1); + results.Messages.Last().Moves.Should().Be(1); } [Fact] - public void AutoRefreshSelected() + public void AutoRefreshTransform() { - //test added as v6 broke unit test in DynamicData.Snippets - var initialItems = Enumerable.Range(1, 10) - .Select(i => new SelectableItem(i)) - .ToArray(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, i)).ToArray(); //result should only be true when all items are set to true - using (var sourceList = new SourceList()) - using (var sut = sourceList.Connect().AutoRefresh().Filter(si => si.IsSelected).AsObservableList()) - { - sourceList.AddRange(initialItems); - sut.Count.Should().Be(0); + using var list = new SourceList(); + using var results = list.Connect().AutoRefresh(p => p.Age).Transform((p, idx) => new TransformedPerson(p, idx)).AsAggregator(); + list.AddRange(items); + + results.Data.Count.Should().Be(100); + results.Messages.Count.Should().Be(1); + + //update an item which did not match the filter and does so after change + items[0].Age = 60; + results.Messages.Count.Should().Be(2); + results.Messages.Last().Refreshes.Should().Be(1); + results.Messages.Last().First().Item.Reason.Should().Be(ListChangeReason.Refresh); + results.Messages.Last().First().Item.Current.Index.Should().Be(0); + + items[60].Age = 160; + results.Messages.Count.Should().Be(3); + results.Messages.Last().Refreshes.Should().Be(1); + results.Messages.Last().First().Item.Reason.Should().Be(ListChangeReason.Refresh); + results.Messages.Last().First().Item.Current.Index.Should().Be(60); + } - initialItems[0].IsSelected = true; - sut.Count.Should().Be(1); + [Fact] + public void RefreshTransformAsList() + { + SourceList list = new SourceList(); + var valueList = list.Connect().AutoRefresh(e => e.Value).Transform(e => e.Value, true).AsObservableList(); - initialItems[1].IsSelected = true; - sut.Count.Should().Be(2); + var obj = new Example { Value = 0 }; + list.Add(obj); + obj.Value = 1; + valueList.Items.First().Should().Be(1); + } + + private class Example : AbstractNotifyPropertyChanged + { + private int _value; - //remove the selected items - sourceList.RemoveRange(0, 2); - sut.Count.Should().Be(0); + public int Value + { + get => _value; + set => SetAndRaise(ref _value, value); } } private class SelectableItem : AbstractNotifyPropertyChanged { - public int Id { get; } + private bool _isSelected; public SelectableItem(int id) { Id = id; } - private bool _isSelected; + public int Id { get; } public bool IsSelected { @@ -426,12 +385,7 @@ public bool IsSelected set => SetAndRaise(ref _isSelected, value); } - protected bool Equals(SelectableItem other) - { - return Id == other.Id; - } - - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -455,33 +409,26 @@ public override int GetHashCode() { return Id; } - } - [Fact] - public void RefreshTransformAsList() - { - SourceList list = new SourceList(); - var valueList = list.Connect() - .AutoRefresh(e => e.Value) - .Transform(e => e.Value, true) - .AsObservableList(); - - var obj = new Example { Value = 0 }; - list.Add(obj); - obj.Value = 1; - valueList.Items.First().Should().Be(1); + protected bool Equals(SelectableItem other) + { + return Id == other.Id; + } } - private class Example : AbstractNotifyPropertyChanged + private class TransformedPerson { - private int _value; - - public int Value + public TransformedPerson(Person person, int index) { - get => _value; - set => SetAndRaise(ref _value, value); + Person = person; + Index = index; } - } + public int Index { get; } + + public Person Person { get; } + + public DateTime TimeStamp { get; } = DateTime.Now; + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/BatchFixture.cs b/src/DynamicData.Tests/List/BatchFixture.cs index ba51b6975..d466cc066 100644 --- a/src/DynamicData.Tests/List/BatchFixture.cs +++ b/src/DynamicData.Tests/List/BatchFixture.cs @@ -1,27 +1,29 @@ using System; +using System.Reactive.Linq; + using DynamicData.Tests.Domain; + +using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; -using System.Reactive.Linq; -using FluentAssertions; namespace DynamicData.Tests.List { - - public class BatchFixture: IDisposable + public class BatchFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; - public BatchFixture() + private readonly ISourceList _source; + + public BatchFixture() { _scheduler = new TestScheduler(); _source = new SourceList(); - _results = _source.Connect() - .Buffer(TimeSpan.FromMinutes(1), _scheduler) - .FlattenBufferResult() - .AsAggregator(); + _results = _source.Connect().Buffer(TimeSpan.FromMinutes(1), _scheduler).FlattenBufferResult().AsAggregator(); } public void Dispose() @@ -47,4 +49,4 @@ public void ResultsWillBeReceivedAfterClosingBuffer() _results.Messages.Count.Should().Be(1, "Should be 1 update"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/BatchIfFixture.cs b/src/DynamicData.Tests/List/BatchIfFixture.cs index 191cbeffe..f24ada473 100644 --- a/src/DynamicData.Tests/List/BatchIfFixture.cs +++ b/src/DynamicData.Tests/List/BatchIfFixture.cs @@ -1,23 +1,28 @@ using System; +using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + +using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; -using FluentAssertions; -using System.Reactive.Linq; namespace DynamicData.Tests.List { - - public class BatchIfFixture: IDisposable + public class BatchIfFixture : IDisposable { - private readonly ISourceList _source; + private readonly ISubject _pausingSubject = new Subject(); + private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; - private readonly ISubject _pausingSubject = new Subject(); + private readonly ISourceList _source; - public BatchIfFixture() + public BatchIfFixture() { _pausingSubject = new Subject(); _scheduler = new TestScheduler(); @@ -25,10 +30,24 @@ public BatchIfFixture() _results = _source.Connect().BufferIf(_pausingSubject, _scheduler).AsAggregator(); } - public void Dispose() + [Fact] + public void CanToggleSuspendResume() { - _results.Dispose(); - _source.Dispose(); + _pausingSubject.OnNext(true); + ////advance otherwise nothing happens + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + + _source.Add(new Person("A", 1)); + + //go forward an arbitary amount of time + _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); + _results.Messages.Count.Should().Be(0, "There should be no messages"); + + _pausingSubject.OnNext(false); + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + _source.Add(new Person("B", 1)); + + _results.Messages.Count.Should().Be(2, "There should be no messages"); } /// @@ -41,11 +60,7 @@ public void ChangesNotLostIfConsumerIsRunningOnDifferentThread() var consumerScheduler = new TestScheduler(); //Note consumer is running on a different scheduler - _source.Connect() - .BufferIf(_pausingSubject, producerScheduler) - .ObserveOn(consumerScheduler) - .Bind(out var target) - .AsAggregator(); + _source.Connect().BufferIf(_pausingSubject, producerScheduler).ObserveOn(consumerScheduler).Bind(out var target).AsAggregator(); _source.Add(new Person("A", 1)); @@ -79,6 +94,12 @@ public void ChangesNotLostIfConsumerIsRunningOnDifferentThread() target.Count.Should().Be(2, "There should be 2 message"); } + public void Dispose() + { + _results.Dispose(); + _source.Dispose(); + } + [Fact] public void NoResultsWillBeReceivedIfPaused() { @@ -102,25 +123,5 @@ public void ResultsWillBeReceivedIfNotPaused() _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); _results.Messages.Count.Should().Be(1, "Should be 1 update"); } - - [Fact] - public void CanToggleSuspendResume() - { - _pausingSubject.OnNext(true); - ////advance otherwise nothing happens - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - - _source.Add(new Person("A", 1)); - - //go forward an arbitary amount of time - _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); - _results.Messages.Count.Should().Be(0, "There should be no messages"); - - _pausingSubject.OnNext(false); - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - _source.Add(new Person("B", 1)); - - _results.Messages.Count.Should().Be(2, "There should be no messages"); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/BatchIfWithTimeOutFixture.cs b/src/DynamicData.Tests/List/BatchIfWithTimeOutFixture.cs index 9037464f5..19c429713 100644 --- a/src/DynamicData.Tests/List/BatchIfWithTimeOutFixture.cs +++ b/src/DynamicData.Tests/List/BatchIfWithTimeOutFixture.cs @@ -1,22 +1,27 @@ using System; using System.Reactive.Subjects; -using FluentAssertions; + using DynamicData.Tests.Domain; + +using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.List { - - public class BatchIfWithTimeOutFixture: IDisposable + public class BatchIfWithTimeOutFixture : IDisposable { - private readonly ISourceList _source; + private readonly ISubject _pausingSubject = new Subject(); + private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; - private readonly ISubject _pausingSubject = new Subject(); + private readonly ISourceList _source; - public BatchIfWithTimeOutFixture() + public BatchIfWithTimeOutFixture() { _pausingSubject = new Subject(); _scheduler = new TestScheduler(); @@ -24,26 +29,31 @@ public BatchIfWithTimeOutFixture() _results = _source.Connect().BufferIf(_pausingSubject, TimeSpan.FromMinutes(1), _scheduler).AsAggregator(); } - public void Dispose() - { - _results.Dispose(); - _source.Dispose(); - _pausingSubject.OnCompleted(); - } - [Fact] - public void WillApplyTimeout() + public void CanToggleSuspendResume() { _pausingSubject.OnNext(true); - - //should timeout - _scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); + ////advance otherwise nothing happens + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); _source.Add(new Person("A", 1)); //go forward an arbitary amount of time - // _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); - _results.Messages.Count.Should().Be(1, "There should be no messages"); + _results.Messages.Count.Should().Be(0, "There should be no messages"); + + _pausingSubject.OnNext(false); + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + + _source.Add(new Person("B", 1)); + + _results.Messages.Count.Should().Be(2, "There should be no messages"); + } + + public void Dispose() + { + _results.Dispose(); + _source.Dispose(); + _pausingSubject.OnCompleted(); } [Fact] @@ -69,23 +79,18 @@ public void ResultsWillBeReceivedIfNotPaused() } [Fact] - public void CanToggleSuspendResume() + public void WillApplyTimeout() { _pausingSubject.OnNext(true); - ////advance otherwise nothing happens - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); + + //should timeout + _scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); _source.Add(new Person("A", 1)); //go forward an arbitary amount of time - _results.Messages.Count.Should().Be(0, "There should be no messages"); - - _pausingSubject.OnNext(false); - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10).Ticks); - - _source.Add(new Person("B", 1)); - - _results.Messages.Count.Should().Be(2, "There should be no messages"); + // _scheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); + _results.Messages.Count.Should().Be(1, "There should be no messages"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/BufferFixture.cs b/src/DynamicData.Tests/List/BufferFixture.cs index 9c091c22b..8575edfad 100644 --- a/src/DynamicData.Tests/List/BufferFixture.cs +++ b/src/DynamicData.Tests/List/BufferFixture.cs @@ -1,20 +1,25 @@ using System; +using System.Reactive.Linq; + using DynamicData.Tests.Domain; + +using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; -using FluentAssertions; -using System.Reactive.Linq; namespace DynamicData.Tests.List { - - public class BufferFixture: IDisposable + public class BufferFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; - public BufferFixture() + private readonly ISourceList _source; + + public BufferFixture() { _scheduler = new TestScheduler(); _source = new SourceList(); @@ -44,4 +49,4 @@ public void ResultsWillBeReceivedAfterClosingBuffer() _results.Messages.Count.Should().Be(1, "Should be 1 update"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/BufferInitialFixture.cs b/src/DynamicData.Tests/List/BufferInitialFixture.cs index 29e043df2..dba1b16fe 100644 --- a/src/DynamicData.Tests/List/BufferInitialFixture.cs +++ b/src/DynamicData.Tests/List/BufferInitialFixture.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.List @@ -17,27 +21,25 @@ public void BufferInitial() { var scheduler = new TestScheduler(); - using (var cache = new SourceList()) - using (var aggregator = cache.Connect().BufferInitial(TimeSpan.FromSeconds(1), scheduler).AsAggregator()) + using var cache = new SourceList(); + using var aggregator = cache.Connect().BufferInitial(TimeSpan.FromSeconds(1), scheduler).AsAggregator(); + foreach (var item in People) { - foreach (var item in People) - { - cache.Add(item); - } + cache.Add(item); + } - aggregator.Data.Count.Should().Be(0); - aggregator.Messages.Count.Should().Be(0); + aggregator.Data.Count.Should().Be(0); + aggregator.Messages.Count.Should().Be(0); - scheduler.Start(); + scheduler.Start(); - aggregator.Data.Count.Should().Be(10_000); - aggregator.Messages.Count.Should().Be(1); + aggregator.Data.Count.Should().Be(10_000); + aggregator.Messages.Count.Should().Be(1); - cache.Add(new Person("_New", 1)); + cache.Add(new Person("_New", 1)); - aggregator.Data.Count.Should().Be(10_001); - aggregator.Messages.Count.Should().Be(2); - } + aggregator.Data.Count.Should().Be(10_001); + aggregator.Messages.Count.Should().Be(2); } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/List/CastFixture.cs b/src/DynamicData.Tests/List/CastFixture.cs index 7dfcc9982..4f41b98ef 100644 --- a/src/DynamicData.Tests/List/CastFixture.cs +++ b/src/DynamicData.Tests/List/CastFixture.cs @@ -1,36 +1,38 @@ using System; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class CastFixture: IDisposable + public class CastFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public CastFixture() - { - _source = new SourceList(); - _results = _source.Cast(i=>(decimal)i).AsAggregator(); - } + private readonly ISourceList _source; - public void Dispose() + public CastFixture() { - _source.Dispose(); - _results.Dispose(); + _source = new SourceList(); + _results = _source.Cast(i => (decimal)i).AsAggregator(); } [Fact] public void CanCast() { - _source.AddRange(Enumerable.Range(1,10)); + _source.AddRange(Enumerable.Range(1, 10)); _results.Data.Count.Should().Be(10); _source.Clear(); _results.Data.Count.Should().Be(0); } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/ChangeAwareListFixture.cs b/src/DynamicData.Tests/List/ChangeAwareListFixture.cs index 97a3d240c..38b29d3d4 100644 --- a/src/DynamicData.Tests/List/ChangeAwareListFixture.cs +++ b/src/DynamicData.Tests/List/ChangeAwareListFixture.cs @@ -1,15 +1,17 @@ using System; using System.Linq; + using DynamicData.Kernel; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class ChangeAwareListFixture { - private ChangeAwareList _list; + private readonly ChangeAwareList _list; public ChangeAwareListFixture() { @@ -32,50 +34,49 @@ public void Add() } [Fact] - public void AddSecond() + public void AddManyInSuccession() { - _list.Add(1); - _list.ClearChanges(); - - _list.Add(2); + Enumerable.Range(1, 10).ForEach(_list.Add); //assert changes var changes = _list.CaptureChanges(); changes.Count.Should().Be(1); - changes.Adds.Should().Be(1); - changes.First().Item.Current.Should().Be(2); + changes.Adds.Should().Be(10); + changes.First().Range.Should().BeEquivalentTo(Enumerable.Range(1, 10)); //assert collection - _list.Should().BeEquivalentTo(Enumerable.Range(1, 2)); + _list.Should().BeEquivalentTo(Enumerable.Range(1, 10)); } [Fact] - public void AddManyInSuccession() + public void AddRange() { - Enumerable.Range(1, 10) - .ForEach(_list.Add); + _list.AddRange(Enumerable.Range(1, 10)); //assert changes var changes = _list.CaptureChanges(); changes.Count.Should().Be(1); changes.Adds.Should().Be(10); changes.First().Range.Should().BeEquivalentTo(Enumerable.Range(1, 10)); + //assert collection _list.Should().BeEquivalentTo(Enumerable.Range(1, 10)); } [Fact] - public void AddRange() + public void AddSecond() { - _list.AddRange(Enumerable.Range(1, 10)); + _list.Add(1); + _list.ClearChanges(); + + _list.Add(2); //assert changes var changes = _list.CaptureChanges(); changes.Count.Should().Be(1); - changes.Adds.Should().Be(10); - changes.First().Range.Should().BeEquivalentTo(Enumerable.Range(1, 10)); - + changes.Adds.Should().Be(1); + changes.First().Item.Current.Should().Be(2); //assert collection - _list.Should().BeEquivalentTo(Enumerable.Range(1, 10)); + _list.Should().BeEquivalentTo(Enumerable.Range(1, 2)); } [Fact] @@ -108,58 +109,74 @@ public void InsertRangeInCentre() changes.First().Range.Should().BeEquivalentTo(Enumerable.Range(1, 10)); changes.Skip(1).First().Range.Should().BeEquivalentTo(Enumerable.Range(11, 10)); - var shouldBe = Enumerable.Range(1, 5) - .Union(Enumerable.Range(11, 10)) - .Union(Enumerable.Range(6, 5)); + var shouldBe = Enumerable.Range(1, 5).Union(Enumerable.Range(11, 10)).Union(Enumerable.Range(6, 5)); //assert collection _list.Should().BeEquivalentTo(shouldBe); } [Fact] - public void Remove() + public void Refresh() { - _list.Add(1); + _list.AddRange(Enumerable.Range(0, 9)); _list.ClearChanges(); + _list.Refresh(1); - _list.Remove(1); + //assert changes (should batch) + var changes = _list.CaptureChanges(); - //assert changes + changes.Count.Should().Be(1); + changes.Refreshes.Should().Be(1); + changes.First().Reason.Should().Be(ListChangeReason.Refresh); + changes.First().Item.Current.Should().Be(1); + + _list.Refresh(5).Should().Be(true); + _list.Refresh(-1).Should().Be(false); + _list.Refresh(1000).Should().Be(false); + } + + [Fact] + public void RefreshAt() + { + _list.AddRange(Enumerable.Range(0, 9)); + _list.ClearChanges(); + _list.RefreshAt(1); + + //assert changes (should batch) var changes = _list.CaptureChanges(); + changes.Count.Should().Be(1); - changes.Removes.Should().Be(1); + changes.Refreshes.Should().Be(1); + changes.First().Reason.Should().Be(ListChangeReason.Refresh); changes.First().Item.Current.Should().Be(1); - //assert collection - _list.Count.Should().Be(0); + + Assert.Throws(() => _list.RefreshAt(-1)); + Assert.Throws(() => _list.RefreshAt(1000)); } [Fact] - public void RemoveRange() + public void Remove() { - _list.AddRange(Enumerable.Range(1, 10)); + _list.Add(1); _list.ClearChanges(); - _list.RemoveRange(5, 3); + _list.Remove(1); //assert changes var changes = _list.CaptureChanges(); changes.Count.Should().Be(1); - changes.Removes.Should().Be(3); - changes.First().Range.Should().BeEquivalentTo(Enumerable.Range(6, 3)); - - //assert collection - var shouldBe = Enumerable.Range(1, 5) - .Union(Enumerable.Range(9, 2)); + changes.Removes.Should().Be(1); + changes.First().Item.Current.Should().Be(1); //assert collection - _list.Should().BeEquivalentTo(shouldBe); + _list.Count.Should().Be(0); } [Fact] - public void RemoveSucession() + public void RemoveMany() { _list.AddRange(Enumerable.Range(1, 10)); _list.ClearChanges(); - _list.ToArray().ForEach(i => _list.Remove(i)); + _list.RemoveMany(Enumerable.Range(1, 10)); //assert changes (should batch)s var changes = _list.CaptureChanges(); @@ -172,29 +189,32 @@ public void RemoveSucession() } [Fact] - public void RemoveSucessionReversed() + public void RemoveRange() { _list.AddRange(Enumerable.Range(1, 10)); _list.ClearChanges(); - _list.OrderByDescending(i => i).ToArray().ForEach(i => _list.Remove(i)); + _list.RemoveRange(5, 3); - //assert changes (should batch) + //assert changes var changes = _list.CaptureChanges(); changes.Count.Should().Be(1); - changes.Removes.Should().Be(10); - changes.First().Range.Should().BeEquivalentTo(Enumerable.Range(1, 10)); + changes.Removes.Should().Be(3); + changes.First().Range.Should().BeEquivalentTo(Enumerable.Range(6, 3)); + //assert collection - _list.Count.Should().Be(0); + var shouldBe = Enumerable.Range(1, 5).Union(Enumerable.Range(9, 2)); + //assert collection + _list.Should().BeEquivalentTo(shouldBe); } [Fact] - public void RemoveMany() + public void RemoveSucession() { _list.AddRange(Enumerable.Range(1, 10)); _list.ClearChanges(); - _list.RemoveMany(Enumerable.Range(1, 10)); + _list.ToArray().ForEach(i => _list.Remove(i)); //assert changes (should batch)s var changes = _list.CaptureChanges(); @@ -207,42 +227,20 @@ public void RemoveMany() } [Fact] - public void RefreshAt() + public void RemoveSucessionReversed() { - _list.AddRange(Enumerable.Range(0, 9)); + _list.AddRange(Enumerable.Range(1, 10)); _list.ClearChanges(); - _list.RefreshAt(1); - - //assert changes (should batch) - var changes = _list.CaptureChanges(); - - changes.Count.Should().Be(1); - changes.Refreshes.Should().Be(1); - changes.First().Reason.Should().Be(ListChangeReason.Refresh); - changes.First().Item.Current.Should().Be(1); - Assert.Throws(() => _list.RefreshAt(-1)); - Assert.Throws(() => _list.RefreshAt(1000)); - } - - [Fact] - public void Refresh() - { - _list.AddRange(Enumerable.Range(0, 9)); - _list.ClearChanges(); - _list.Refresh(1); + _list.OrderByDescending(i => i).ToArray().ForEach(i => _list.Remove(i)); //assert changes (should batch) var changes = _list.CaptureChanges(); - changes.Count.Should().Be(1); - changes.Refreshes.Should().Be(1); - changes.First().Reason.Should().Be(ListChangeReason.Refresh); - changes.First().Item.Current.Should().Be(1); - - _list.Refresh(5).Should().Be(true); - _list.Refresh(-1).Should().Be(false); - _list.Refresh(1000).Should().Be(false); + changes.Removes.Should().Be(10); + changes.First().Range.Should().BeEquivalentTo(Enumerable.Range(1, 10)); + //assert collection + _list.Count.Should().Be(0); } [Fact] @@ -264,4 +262,4 @@ public void ThrowWhenRemovingRangeThatFinishesOutsideOfBoundaries() Assert.Throws(() => _list.RemoveRange(0, 2)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/CloneChangesFixture.cs b/src/DynamicData.Tests/List/CloneChangesFixture.cs index 1d8c50c15..e147c1b5a 100644 --- a/src/DynamicData.Tests/List/CloneChangesFixture.cs +++ b/src/DynamicData.Tests/List/CloneChangesFixture.cs @@ -1,19 +1,22 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; + using DynamicData.Kernel; -using Xunit; -using System.Collections.ObjectModel; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - public class CloneChangesFixture { - private readonly ChangeAwareList _source; private readonly List _clone; - public CloneChangesFixture() + private readonly ChangeAwareList _source; + + public CloneChangesFixture() { _source = new ChangeAwareList(); _clone = new List(); @@ -32,10 +35,9 @@ public void Add() } [Fact] - public void AddSecond() + public void AddManyInSuccession() { - _source.Add(1); - _source.Add(2); + Enumerable.Range(1, 10).ForEach(_source.Add); var changes = _source.CaptureChanges(); _clone.Clone(changes); @@ -43,10 +45,9 @@ public void AddSecond() } [Fact] - public void AddManyInSuccession() + public void AddRange() { - Enumerable.Range(1, 10) - .ForEach(_source.Add); + _source.AddRange(Enumerable.Range(1, 10)); var changes = _source.CaptureChanges(); _clone.Clone(changes); @@ -54,9 +55,10 @@ public void AddManyInSuccession() } [Fact] - public void AddRange() + public void AddSecond() { - _source.AddRange(Enumerable.Range(1, 10)); + _source.Add(1); + _source.Add(2); var changes = _source.CaptureChanges(); _clone.Clone(changes); @@ -85,46 +87,59 @@ public void InsertRangeInCentre() } [Fact] - public void Remove() + public void MovedItemInListHigherToLowerIsMoved() { - _source.Add(1); - _source.Remove(1); + _source.AddRange(Enumerable.Range(1, 10)); + _source.Move(2, 1); var changes = _source.CaptureChanges(); + _clone.Clone(changes); + _clone.Should().BeEquivalentTo(_source); } [Fact] - public void RemoveRange() + public void MovedItemInListLowerToHigherIsMoved() { _source.AddRange(Enumerable.Range(1, 10)); - _source.RemoveRange(5, 3); + _source.Move(1, 2); var changes = _source.CaptureChanges(); + _clone.Clone(changes); + _clone.Should().BeEquivalentTo(_source); } [Fact] - public void RemoveSucession() + public void MovedItemInObservableCollectionIsMoved() { _source.AddRange(Enumerable.Range(1, 10)); - _source.ClearChanges(); + _source.Move(1, 2); - _source.ToArray().ForEach(i => _source.Remove(i)); + var clone = new ObservableCollection(); var changes = _source.CaptureChanges(); - _clone.Clone(changes); - _clone.Should().BeEquivalentTo(_source); + var itemMoved = false; + + clone.CollectionChanged += (s, e) => + { + if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Move) + { + itemMoved = true; + } + }; + + clone.Clone(changes); + + itemMoved.Should().BeTrue(); } [Fact] - public void RemoveSucessionReversed() + public void Remove() { - _source.AddRange(Enumerable.Range(1, 10)); - _source.ClearChanges(); - - _source.OrderByDescending(i => i).ToArray().ForEach(i => _source.Remove(i)); + _source.Add(1); + _source.Remove(1); var changes = _source.CaptureChanges(); _clone.Clone(changes); @@ -132,22 +147,22 @@ public void RemoveSucessionReversed() } [Fact] - public void RemoveMany() + public void RemoveInnerRange() { _source.AddRange(Enumerable.Range(1, 10)); - _source.RemoveMany(Enumerable.Range(1, 10)); + _source.RemoveRange(5, 3); var changes = _source.CaptureChanges(); _clone.Clone(changes); _clone.Should().BeEquivalentTo(_source); } [Fact] - public void RemoveInnerRange() + public void RemoveMany() { _source.AddRange(Enumerable.Range(1, 10)); - _source.RemoveRange(5, 3); + _source.RemoveMany(Enumerable.Range(1, 10)); var changes = _source.CaptureChanges(); _clone.Clone(changes); _clone.Should().BeEquivalentTo(_source); @@ -165,53 +180,39 @@ public void RemoveManyPartial() } [Fact] - public void MovedItemInObservableCollectionIsMoved() + public void RemoveRange() { _source.AddRange(Enumerable.Range(1, 10)); - _source.Move(1, 2); + _source.RemoveRange(5, 3); - var clone = new ObservableCollection(); var changes = _source.CaptureChanges(); - var itemMoved = false; - - clone.CollectionChanged += (s, e) => - { - if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Move) - { - itemMoved = true; - } - }; - - clone.Clone(changes); - - itemMoved.Should().BeTrue(); + _clone.Clone(changes); + _clone.Should().BeEquivalentTo(_source); } [Fact] - public void MovedItemInListLowerToHigherIsMoved() - { + public void RemoveSucession() + { _source.AddRange(Enumerable.Range(1, 10)); - _source.Move(1, 2); + _source.ClearChanges(); + _source.ToArray().ForEach(i => _source.Remove(i)); var changes = _source.CaptureChanges(); - _clone.Clone(changes); - _clone.Should().BeEquivalentTo(_source); } [Fact] - public void MovedItemInListHigherToLowerIsMoved() + public void RemoveSucessionReversed() { _source.AddRange(Enumerable.Range(1, 10)); - _source.Move(2, 1); + _source.ClearChanges(); - var changes = _source.CaptureChanges(); + _source.OrderByDescending(i => i).ToArray().ForEach(i => _source.Remove(i)); + var changes = _source.CaptureChanges(); _clone.Clone(changes); - _clone.Should().BeEquivalentTo(_source); } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/CloneFixture.cs b/src/DynamicData.Tests/List/CloneFixture.cs index 4b88fc51b..289260051 100644 --- a/src/DynamicData.Tests/List/CloneFixture.cs +++ b/src/DynamicData.Tests/List/CloneFixture.cs @@ -2,34 +2,30 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; + using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class CloneFixture: IDisposable + public class CloneFixture : IDisposable { + private readonly IDisposable _cloner; + private readonly ICollection _collection = new Collection(); - private readonly ISourceCache _source; - private readonly IDisposable _cloner; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + private readonly RandomPersonGenerator _generator = new(); + + private readonly ISourceCache _source; - public CloneFixture() + public CloneFixture() { _collection = new Collection(); _source = new SourceCache(p => p.Name); - _cloner = _source.Connect() - .Clone(_collection) - .Subscribe(); - } - - public void Dispose() - { - _cloner.Dispose(); - _source.Dispose(); + _cloner = _source.Connect().Clone(_collection).Subscribe(); } [Fact] @@ -43,15 +39,28 @@ public void AddToSourceAddsToDestination() } [Fact] - public void UpdateToSourceUpdatesTheDestination() + public void BatchAdd() { - var person = new Person("Adult1", 50); - var personUpdated = new Person("Adult1", 51); - _source.AddOrUpdate(person); - _source.AddOrUpdate(personUpdated); + var people = _generator.Take(100).ToList(); + _source.AddOrUpdate(people); - _collection.Count.Should().Be(1, "Should be 1 item in the collection"); - _collection.First().Should().Be(personUpdated, "Should be updated person"); + _collection.Count.Should().Be(100, "Should be 100 items in the collection"); + _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); + } + + [Fact] + public void BatchRemove() + { + var people = _generator.Take(100).ToList(); + _source.AddOrUpdate(people); + _source.Clear(); + _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + } + + public void Dispose() + { + _cloner.Dispose(); + _source.Dispose(); } [Fact] @@ -65,22 +74,15 @@ public void RemoveSourceRemovesFromTheDestination() } [Fact] - public void BatchAdd() + public void UpdateToSourceUpdatesTheDestination() { - var people = _generator.Take(100).ToList(); - _source.AddOrUpdate(people); - - _collection.Count.Should().Be(100, "Should be 100 items in the collection"); - _collection.Should().BeEquivalentTo(_collection, "Collections should be equivalent"); - } + var person = new Person("Adult1", 50); + var personUpdated = new Person("Adult1", 51); + _source.AddOrUpdate(person); + _source.AddOrUpdate(personUpdated); - [Fact] - public void BatchRemove() - { - var people = _generator.Take(100).ToList(); - _source.AddOrUpdate(people); - _source.Clear(); - _collection.Count.Should().Be(0, "Should be 100 items in the collection"); + _collection.Count.Should().Be(1, "Should be 1 item in the collection"); + _collection.First().Should().Be(personUpdated, "Should be updated person"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/CreationFixtures.cs b/src/DynamicData.Tests/List/CreationFixtures.cs index 3935a6789..458c69660 100644 --- a/src/DynamicData.Tests/List/CreationFixtures.cs +++ b/src/DynamicData.Tests/List/CreationFixtures.cs @@ -1,37 +1,37 @@ using System; using System.Reactive.Linq; using System.Threading.Tasks; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class ListCreationFixtures { - [Fact] public void Create() { Task CreateTask(T value) => Task.FromResult(value); - SubscribeAndAssert(ObservableChangeSet.Create(async list => - { - var value = await CreateTask(10); - list.Add(value); - return () => { }; - })); + SubscribeAndAssert( + ObservableChangeSet.Create( + async list => + { + var value = await CreateTask(10); + list.Add(value); + return () => { }; + })); } private void SubscribeAndAssert(IObservable> observableChangeset, bool expectsError = false) { - Exception error = null; + Exception? error = null; bool complete = false; - IChangeSet changes = null; + IChangeSet? changes = null; - using (var myList = observableChangeset - .Finally(()=> complete = true) - .AsObservableList()) + using (var myList = observableChangeset.Finally(() => complete = true).AsObservableList()) using (myList.Connect().Subscribe(result => changes = result, ex => error = ex)) { if (!expectsError) @@ -47,4 +47,4 @@ private void SubscribeAndAssert(IObservable> observableChangese complete.Should().BeTrue(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/DeferUntilLoadedFixture.cs b/src/DynamicData.Tests/List/DeferUntilLoadedFixture.cs index e7883e94a..b070809eb 100644 --- a/src/DynamicData.Tests/List/DeferUntilLoadedFixture.cs +++ b/src/DynamicData.Tests/List/DeferUntilLoadedFixture.cs @@ -1,28 +1,30 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class DeferAnsdSkipFixture { [Fact] public void DeferUntilLoadedDoesNothingUntilDataHasBeenReceived() { bool updateReceived = false; - IChangeSet result = null; + IChangeSet? result = null; var cache = new SourceList(); - var deferStream = cache.Connect().DeferUntilLoaded() - .Subscribe(changes => - { - updateReceived = true; - result = changes; - }); + var deferStream = cache.Connect().DeferUntilLoaded().Subscribe( + changes => + { + updateReceived = true; + result = changes; + }); var person = new Person("Test", 1); @@ -30,6 +32,12 @@ public void DeferUntilLoadedDoesNothingUntilDataHasBeenReceived() cache.Add(person); updateReceived.Should().BeTrue(); + + if (result is null) + { + throw new InvalidOperationException(nameof(result)); + } + result.Adds.Should().Be(1); result.Unified().First().Current.Should().Be(person); deferStream.Dispose(); @@ -42,8 +50,7 @@ public void SkipInitialDoesNotReturnTheFirstBatchOfData() var cache = new SourceList(); - var deferStream = cache.Connect().SkipInitial() - .Subscribe(changes => updateReceived = true); + var deferStream = cache.Connect().SkipInitial().Subscribe(changes => updateReceived = true); updateReceived.Should().BeFalse(); @@ -56,4 +63,4 @@ public void SkipInitialDoesNotReturnTheFirstBatchOfData() deferStream.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/DisposeManyFixture.cs b/src/DynamicData.Tests/List/DisposeManyFixture.cs index 60fe9b2ba..2dd599abf 100644 --- a/src/DynamicData.Tests/List/DisposeManyFixture.cs +++ b/src/DynamicData.Tests/List/DisposeManyFixture.cs @@ -1,22 +1,34 @@ using System; using System.Linq; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class DisposeManyFixture: IDisposable + public class DisposeManyFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public DisposeManyFixture() + private readonly ISourceList _source; + + public DisposeManyFixture() { _source = new SourceList(); _results = new ChangeSetAggregator(_source.Connect().DisposeMany()); } + [Fact] + public void AddWillNotCallDispose() + { + _source.Add(new DisposableObject(1)); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Items.First().IsDisposed.Should().Be(false, "Should not be disposed"); + } + public void Dispose() { _source.Dispose(); @@ -24,13 +36,16 @@ public void Dispose() } [Fact] - public void AddWillNotCallDispose() + public void EverythingIsDisposedWhenStreamIsDisposed() { - _source.Add(new DisposableObject(1)); + var toadd = Enumerable.Range(1, 10).Select(i => new DisposableObject(i)); + _source.AddRange(toadd); + _source.Clear(); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - _results.Data.Items.First().IsDisposed.Should().Be(false, "Should not be disposed"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + + var itemsCleared = _results.Messages[1].First().Range; + itemsCleared.All(d => d.IsDisposed).Should().BeTrue(); } [Fact] @@ -56,33 +71,21 @@ public void UpdateWillCallDispose() _results.Messages[1].First().Item.Previous.Value.IsDisposed.Should().Be(true, "Previous should be disposed"); } - [Fact] - public void EverythingIsDisposedWhenStreamIsDisposed() - { - var toadd = Enumerable.Range(1, 10).Select(i => new DisposableObject(i)); - _source.AddRange(toadd); - _source.Clear(); - - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - - var itemsCleared = _results.Messages[1].First().Range; - itemsCleared.All(d => d.IsDisposed).Should().BeTrue(); - } - private class DisposableObject : IDisposable { - public bool IsDisposed { get; private set; } - public int Id { get; private set; } - public DisposableObject(int id) { Id = id; } + public int Id { get; private set; } + + public bool IsDisposed { get; private set; } + public void Dispose() { IsDisposed = true; } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/DistinctValuesFixture.cs b/src/DynamicData.Tests/List/DistinctValuesFixture.cs index 759b65e8b..5bb09a524 100644 --- a/src/DynamicData.Tests/List/DistinctValuesFixture.cs +++ b/src/DynamicData.Tests/List/DistinctValuesFixture.cs @@ -1,29 +1,66 @@ using System; -using DynamicData.Tests.Domain; -using Xunit; using System.Linq; + +using DynamicData.Tests.Domain; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class DistinctValuesFixture: IDisposable + public class DistinctValuesFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public DistinctValuesFixture() + private readonly ISourceList _source; + + public DistinctValuesFixture() { _source = new SourceList(); _results = _source.Connect().DistinctValues(p => p.Age).AsAggregator(); } + [Fact] + public void AddingRemovedItem() + { + var person = new Person("A", 20); + + _source.Add(person); + _source.Remove(person); + _source.Add(person); + + _results.Messages.Count.Should().Be(3, "Should be 2 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + + _results.Data.Items.Should().BeEquivalentTo(new[] { 20 }); + _results.Messages.ElementAt(0).Adds.Should().Be(1, "First message should be an add"); + _results.Messages.ElementAt(1).Removes.Should().Be(1, "Second message should be a remove"); + _results.Messages.ElementAt(2).Adds.Should().Be(1, "Third message should be an add"); + } + public void Dispose() { _source.Dispose(); _results.Dispose(); } + [Fact] + public void DuplicatedResultsResultInNoAdditionalMessage() + { + _source.Edit( + list => + { + list.Add(new Person("Person1", 20)); + list.Add(new Person("Person1", 20)); + list.Add(new Person("Person1", 20)); + }); + + _results.Messages.Count.Should().Be(1, "Should be 1 update message"); + _results.Data.Count.Should().Be(1, "Should be 1 items in the cache"); + _results.Data.Items.First().Should().Be(20, "Should 20"); + } + [Fact] public void FiresAddWhenaNewItemIsAdded() { @@ -37,32 +74,18 @@ public void FiresAddWhenaNewItemIsAdded() [Fact] public void FiresBatchResultOnce() { - _source.Edit(list => - { - list.Add(new Person("Person1", 20)); - list.Add(new Person("Person2", 21)); - list.Add(new Person("Person3", 22)); - }); + _source.Edit( + list => + { + list.Add(new Person("Person1", 20)); + list.Add(new Person("Person2", 21)); + list.Add(new Person("Person3", 22)); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Data.Count.Should().Be(3, "Should be 3 items in the cache"); - _results.Data.Items.Should().BeEquivalentTo(new[] {20, 21, 22}); - _results.Data.Items.First().Should().Be(20, "Should 20"); - } - - [Fact] - public void DuplicatedResultsResultInNoAdditionalMessage() - { - _source.Edit(list => - { - list.Add(new Person("Person1", 20)); - list.Add(new Person("Person1", 20)); - list.Add(new Person("Person1", 20)); - }); - - _results.Messages.Count.Should().Be(1, "Should be 1 update message"); - _results.Data.Count.Should().Be(1, "Should be 1 items in the cache"); + _results.Data.Items.Should().BeEquivalentTo(new[] { 20, 21, 22 }); _results.Data.Items.First().Should().Be(20, "Should 20"); } @@ -94,23 +117,5 @@ public void Replacing() _results.Messages.First().Adds.Should().Be(1, "First message should be an add"); _results.Messages.Skip(1).First().Count.Should().Be(2, "Second messsage should be an add an a remove"); } - - [Fact] - public void AddingRemovedItem() - { - var person = new Person("A", 20); - - _source.Add(person); - _source.Remove(person); - _source.Add(person); - - _results.Messages.Count.Should().Be(3, "Should be 2 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - - _results.Data.Items.Should().BeEquivalentTo(new[] { 20 }); - _results.Messages.ElementAt(0).Adds.Should().Be(1, "First message should be an add"); - _results.Messages.ElementAt(1).Removes.Should().Be(1, "Second message should be a remove"); - _results.Messages.ElementAt(2).Adds.Should().Be(1, "Third message should be an add"); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/DynamicAndFixture.cs b/src/DynamicData.Tests/List/DynamicAndFixture.cs index a95ff4610..df8bc9e3d 100644 --- a/src/DynamicData.Tests/List/DynamicAndFixture.cs +++ b/src/DynamicData.Tests/List/DynamicAndFixture.cs @@ -1,21 +1,25 @@ using System; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class DynamicAndFixture: IDisposable + public class DynamicAndFixture : IDisposable { + private readonly ChangeSetAggregator _results; + + private readonly ISourceList>> _source; + private readonly ISourceList _source1; + private readonly ISourceList _source2; - private readonly ISourceList _source3; - private readonly ISourceList>> _source; - private readonly ChangeSetAggregator _results; + private readonly ISourceList _source3; - public DynamicAndFixture() + public DynamicAndFixture() { _source1 = new SourceList(); _source2 = new SourceList(); @@ -24,42 +28,39 @@ public DynamicAndFixture() _results = _source.And().AsAggregator(); } - public void Dispose() - { - _source1.Dispose(); - _source2.Dispose(); - _source3.Dispose(); - _source.Dispose(); - _results.Dispose(); - } - [Fact] - public void ExcludedWhenItemIsInOneSource() + public void AddAndRemoveLists() { + _source1.AddRange(Enumerable.Range(1, 5)); + _source3.AddRange(Enumerable.Range(1, 5)); + _source.Add(_source1.Connect()); + _source.Add(_source3.Connect()); + + var result = Enumerable.Range(1, 5).ToArray(); + + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(result); + + _source2.AddRange(Enumerable.Range(6, 5)); + _results.Data.Count.Should().Be(5); + _source.Add(_source2.Connect()); - _source1.Add(1); _results.Data.Count.Should().Be(0); - } - [Fact] - public void IncludedWhenItemIsInTwoSources() - { - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - _source1.Add(1); - _source2.Add(1); - _results.Data.Count.Should().Be(1); + _source.RemoveAt(2); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(result); } [Fact] - public void RemovedWhenNoLongerInBoth() + public void ClearOneClearsResult() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.Add(1); - _source2.Add(1); - _source1.Remove(1); + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(1, 5)); + _source1.Clear(); _results.Data.Count.Should().Be(0); } @@ -74,40 +75,43 @@ public void CombineRange() _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); } + public void Dispose() + { + _source1.Dispose(); + _source2.Dispose(); + _source3.Dispose(); + _source.Dispose(); + _results.Dispose(); + } + [Fact] - public void ClearOneClearsResult() + public void ExcludedWhenItemIsInOneSource() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(1, 5)); - _source1.Clear(); + _source1.Add(1); _results.Data.Count.Should().Be(0); } [Fact] - public void AddAndRemoveLists() + public void IncludedWhenItemIsInTwoSources() { - _source1.AddRange(Enumerable.Range(1, 5)); - _source3.AddRange(Enumerable.Range(1, 5)); - _source.Add(_source1.Connect()); - _source.Add(_source3.Connect()); - - var result = Enumerable.Range(1, 5).ToArray(); - - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(result); - - _source2.AddRange(Enumerable.Range(6, 5)); - _results.Data.Count.Should().Be(5); + _source.Add(_source2.Connect()); + _source1.Add(1); + _source2.Add(1); + _results.Data.Count.Should().Be(1); + } + [Fact] + public void RemovedWhenNoLongerInBoth() + { + _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); + _source1.Add(1); + _source2.Add(1); + _source1.Remove(1); _results.Data.Count.Should().Be(0); - - _source.RemoveAt(2); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(result); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/DynamicExceptFixture.cs b/src/DynamicData.Tests/List/DynamicExceptFixture.cs index 63bb8bb9a..de0b3f599 100644 --- a/src/DynamicData.Tests/List/DynamicExceptFixture.cs +++ b/src/DynamicData.Tests/List/DynamicExceptFixture.cs @@ -1,21 +1,25 @@ using System; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class DynamicExceptFixture: IDisposable + public class DynamicExceptFixture : IDisposable { + private readonly ChangeSetAggregator _results; + + private readonly ISourceList>> _source; + private readonly ISourceList _source1; + private readonly ISourceList _source2; - private readonly ISourceList _source3; - private readonly ISourceList>> _source; - private readonly ChangeSetAggregator _results; + private readonly ISourceList _source3; - public DynamicExceptFixture() + public DynamicExceptFixture() { _source1 = new SourceList(); _source2 = new SourceList(); @@ -24,41 +28,47 @@ public DynamicExceptFixture() _results = _source.Except().AsAggregator(); } - public void Dispose() - { - _source1.Dispose(); - _source2.Dispose(); - _source3.Dispose(); - _source.Dispose(); - _results.Dispose(); - } - [Fact] - public void IncludedWhenItemIsInOneSource() + public void AddAndRemoveLists() { - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - _source1.Add(1); - _results.Data.Count.Should().Be(1); - } + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _source3.AddRange(Enumerable.Range(100, 5)); - [Fact] - public void NothingFromOther() - { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source2.Add(1); - _results.Data.Count.Should().Be(0); - } + _source.Add(_source3.Connect()); + + var result = Enumerable.Range(1, 5); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(result); + + _source2.Edit( + innerList => + { + innerList.Clear(); + innerList.AddRange(Enumerable.Range(3, 5)); + }); + + result = Enumerable.Range(1, 2); + _results.Data.Count.Should().Be(2); + _results.Data.Items.Should().BeEquivalentTo(result); + + _source.RemoveAt(1); + result = Enumerable.Range(1, 5); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(result); - [Fact] - public void ExcludedWhenItemIsInTwoSources() - { - _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.Add(1); - _source2.Add(1); - _results.Data.Count.Should().Be(0); + result = Enumerable.Range(1, 2); + _results.Data.Count.Should().Be(2); + _results.Data.Items.Should().BeEquivalentTo(result); + + //remove root except + _source.RemoveAt(0); + result = Enumerable.Range(100, 5); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(result); } [Fact] @@ -72,17 +82,6 @@ public void AddedWhenNoLongerInSecond() _results.Data.Count.Should().Be(1); } - [Fact] - public void CombineRange() - { - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - _source1.AddRange(Enumerable.Range(1, 10)); - _source2.AddRange(Enumerable.Range(6, 10)); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 5)); - } - [Fact] public void ClearFirstClearsResult() { @@ -109,45 +108,51 @@ public void ClearSecondEnsuresFirstIsIncluded() } [Fact] - public void AddAndRemoveLists() + public void CombineRange() { - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _source3.AddRange(Enumerable.Range(100, 5)); - _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - - var result = Enumerable.Range(1, 5); + _source1.AddRange(Enumerable.Range(1, 10)); + _source2.AddRange(Enumerable.Range(6, 10)); _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(result); - - _source2.Edit(innerList => - { - innerList.Clear(); - innerList.AddRange(Enumerable.Range(3, 5)); - }); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 5)); + } - result = Enumerable.Range(1, 2); - _results.Data.Count.Should().Be(2); - _results.Data.Items.Should().BeEquivalentTo(result); + public void Dispose() + { + _source1.Dispose(); + _source2.Dispose(); + _source3.Dispose(); + _source.Dispose(); + _results.Dispose(); + } - _source.RemoveAt(1); - result = Enumerable.Range(1, 5); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(result); + [Fact] + public void ExcludedWhenItemIsInTwoSources() + { + _source.Add(_source1.Connect()); + _source.Add(_source2.Connect()); + _source1.Add(1); + _source2.Add(1); + _results.Data.Count.Should().Be(0); + } + [Fact] + public void IncludedWhenItemIsInOneSource() + { + _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - result = Enumerable.Range(1, 2); - _results.Data.Count.Should().Be(2); - _results.Data.Items.Should().BeEquivalentTo(result); + _source1.Add(1); + _results.Data.Count.Should().Be(1); + } - //remove root except - _source.RemoveAt(0); - result = Enumerable.Range(100, 5); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(result); + [Fact] + public void NothingFromOther() + { + _source.Add(_source1.Connect()); + _source.Add(_source2.Connect()); + _source2.Add(1); + _results.Data.Count.Should().Be(0); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/DynamicOrFixture.cs b/src/DynamicData.Tests/List/DynamicOrFixture.cs index 7982fb611..5952a8ac2 100644 --- a/src/DynamicData.Tests/List/DynamicOrFixture.cs +++ b/src/DynamicData.Tests/List/DynamicOrFixture.cs @@ -1,6 +1,8 @@ using System; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List @@ -18,7 +20,7 @@ public void RefreshPassesThrough() source1.Add(new Item("A")); source2.Add(new Item("B")); source.AddRange(new[] { source1.Connect().AutoRefresh(), source2.Connect().AutoRefresh() }); - + source1.Items.ElementAt(0).Name = "Test"; results.Data.Count.Should().Be(2); @@ -28,16 +30,19 @@ public void RefreshPassesThrough() } } - public class DynamicOrFixture: IDisposable + public class DynamicOrFixture : IDisposable { + private readonly ChangeSetAggregator _results; + + private readonly ISourceList>> _source; + private readonly ISourceList _source1; + private readonly ISourceList _source2; - private readonly ISourceList _source3; - private readonly ISourceList>> _source; - private readonly ChangeSetAggregator _results; + private readonly ISourceList _source3; - public DynamicOrFixture() + public DynamicOrFixture() { _source1 = new SourceList(); _source2 = new SourceList(); @@ -46,27 +51,39 @@ public DynamicOrFixture() _results = _source.Or().AsAggregator(); } - public void Dispose() + [Fact] + public void AddAndRemoveLists() { - _source1.Dispose(); - _source2.Dispose(); - _source3.Dispose(); - _source.Dispose(); - _results.Dispose(); + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _source3.AddRange(Enumerable.Range(100, 5)); + + _source.Add(_source1.Connect()); + _source.Add(_source2.Connect()); + _source.Add(_source3.Connect()); + + var result = Enumerable.Range(1, 5).Union(Enumerable.Range(6, 5)).Union(Enumerable.Range(100, 5)); + + _results.Data.Count.Should().Be(15); + _results.Data.Items.Should().BeEquivalentTo(result); + + _source.RemoveAt(1); + _results.Data.Count.Should().Be(10); + + result = Enumerable.Range(1, 5).Union(Enumerable.Range(100, 5)); + _results.Data.Items.Should().BeEquivalentTo(result); } [Fact] - public void ItemIsReplaced() + public void ClearOnlyClearsOneSource() { - _source1.Add(0); - _source2.Add(1); _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.ReplaceAt(0, 9); - - _results.Data.Count.Should().Be(2); - _results.Messages.Count.Should().Be(3); - _results.Data.Items.Should().BeEquivalentTo(9, 1); + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _source1.Clear(); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); } [Fact] @@ -82,98 +99,86 @@ public void ClearSource() } [Fact] - public void IncludedWhenItemIsInOneSource() + public void CombineRange() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.Add(1); + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _results.Data.Count.Should().Be(10); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); + } - _results.Data.Count.Should().Be(1); - _results.Data.Items.First().Should().Be(1); + public void Dispose() + { + _source1.Dispose(); + _source2.Dispose(); + _source3.Dispose(); + _source.Dispose(); + _results.Dispose(); } [Fact] - public void IncludedWhenItemIsInTwoSources() + public void IncludedWhenItemIsInOneSource() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); _source1.Add(1); - _source2.Add(1); + _results.Data.Count.Should().Be(1); _results.Data.Items.First().Should().Be(1); } [Fact] - public void RemovedWhenNoLongerInEither() + public void IncludedWhenItemIsInTwoSources() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); _source1.Add(1); - _source1.Remove(1); - _results.Data.Count.Should().Be(0); + _source2.Add(1); + _results.Data.Count.Should().Be(1); + _results.Data.Items.First().Should().Be(1); } [Fact] - public void CombineRange() + public void ItemIsReplaced() { + _source1.Add(0); + _source2.Add(1); _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _results.Data.Count.Should().Be(10); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); - } + _source1.ReplaceAt(0, 9); - [Fact] - public void ClearOnlyClearsOneSource() - { - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _source1.Clear(); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); + _results.Data.Count.Should().Be(2); + _results.Messages.Count.Should().Be(3); + _results.Data.Items.Should().BeEquivalentTo(9, 1); } [Fact] - public void AddAndRemoveLists() + public void RemoveAllLists() { _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); + _source3.AddRange(Enumerable.Range(100, 5)); _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); _source.Add(_source3.Connect()); - var result = Enumerable.Range(1, 5).Union(Enumerable.Range(6, 5)).Union(Enumerable.Range(100, 5)); - - _results.Data.Count.Should().Be(15); - _results.Data.Items.Should().BeEquivalentTo(result); - - _source.RemoveAt(1); - _results.Data.Count.Should().Be(10); + _source2.AddRange(Enumerable.Range(6, 5)); + _source.Clear(); - result = Enumerable.Range(1, 5).Union(Enumerable.Range(100, 5)); - _results.Data.Items.Should().BeEquivalentTo(result); + _results.Data.Count.Should().Be(0); } [Fact] - public void RemoveAllLists() + public void RemovedWhenNoLongerInEither() { - _source1.AddRange(Enumerable.Range(1, 5)); - - _source3.AddRange(Enumerable.Range(100, 5)); - _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - - _source2.AddRange(Enumerable.Range(6, 5)); - _source.Clear(); - + _source1.Add(1); + _source1.Remove(1); _results.Data.Count.Should().Be(0); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/DynamicXOrFixture.cs b/src/DynamicData.Tests/List/DynamicXOrFixture.cs index 1cf66b48e..20f41a5fc 100644 --- a/src/DynamicData.Tests/List/DynamicXOrFixture.cs +++ b/src/DynamicData.Tests/List/DynamicXOrFixture.cs @@ -1,21 +1,25 @@ using System; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class DynamicXOrFixture: IDisposable + public class DynamicXOrFixture : IDisposable { + private readonly ChangeSetAggregator _results; + + private readonly ISourceList>> _source; + private readonly ISourceList _source1; + private readonly ISourceList _source2; - private readonly ISourceList _source3; - private readonly ISourceList>> _source; - private readonly ChangeSetAggregator _results; + private readonly ISourceList _source3; - public DynamicXOrFixture() + public DynamicXOrFixture() { _source1 = new SourceList(); _source2 = new SourceList(); @@ -24,78 +28,83 @@ public DynamicXOrFixture() _results = _source.Xor().AsAggregator(); } - public void Dispose() - { - _source1.Dispose(); - _source2.Dispose(); - _source3.Dispose(); - _source.Dispose(); - _results.Dispose(); - } - [Fact] - public void IncludedWhenItemIsInOneSource() + public void AddAndRemoveLists() { + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _source3.AddRange(Enumerable.Range(1, 5)); + _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.Add(1); + _source.Add(_source3.Connect()); - _results.Data.Count.Should().Be(1); - _results.Data.Items.First().Should().Be(1); + var result = Enumerable.Range(6, 5); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(result); + + _source.RemoveAt(0); + result = Enumerable.Range(1, 5).Union(Enumerable.Range(6, 5)); + _results.Data.Count.Should().Be(10); + _results.Data.Items.Should().BeEquivalentTo(result); + + _source.Add(_source1.Connect()); + result = Enumerable.Range(6, 5); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(result); } [Fact] - public void NotIncludedWhenItemIsInTwoSources() + public void ClearOnlyClearsOneSource() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.Add(1); - _source2.Add(1); - _results.Data.Count.Should().Be(0); + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _source1.Clear(); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); } [Fact] - public void RemovedWhenNoLongerInBoth() + public void CombineRange() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.Add(1); - _source2.Add(1); - _source1.Remove(1); - _results.Data.Count.Should().Be(1); + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _results.Data.Count.Should().Be(10); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); } - [Fact] - public void RemovedWhenNoLongerInEither() + public void Dispose() { - _source.Add(_source1.Connect()); - _source.Add(_source2.Connect()); - _source1.Add(1); - _source1.Remove(1); - _results.Data.Count.Should().Be(0); + _source1.Dispose(); + _source2.Dispose(); + _source3.Dispose(); + _source.Dispose(); + _results.Dispose(); } [Fact] - public void CombineRange() + public void IncludedWhenItemIsInOneSource() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _results.Data.Count.Should().Be(10); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); + _source1.Add(1); + + _results.Data.Count.Should().Be(1); + _results.Data.Items.First().Should().Be(1); } [Fact] - public void ClearOnlyClearsOneSource() + public void NotIncludedWhenItemIsInTwoSources() { _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _source1.Clear(); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); + _source1.Add(1); + _source2.Add(1); + _results.Data.Count.Should().Be(0); } [Fact] @@ -111,29 +120,24 @@ public void OverlappingRangeExcludesIntersect() } [Fact] - public void AddAndRemoveLists() + public void RemovedWhenNoLongerInBoth() { - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _source3.AddRange(Enumerable.Range(1, 5)); - _source.Add(_source1.Connect()); _source.Add(_source2.Connect()); - _source.Add(_source3.Connect()); - - var result = Enumerable.Range(6, 5); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(result); - - _source.RemoveAt(0); - result = Enumerable.Range(1, 5).Union(Enumerable.Range(6, 5)); - _results.Data.Count.Should().Be(10); - _results.Data.Items.Should().BeEquivalentTo(result); + _source1.Add(1); + _source2.Add(1); + _source1.Remove(1); + _results.Data.Count.Should().Be(1); + } + [Fact] + public void RemovedWhenNoLongerInEither() + { _source.Add(_source1.Connect()); - result = Enumerable.Range(6, 5); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(result); + _source.Add(_source2.Connect()); + _source1.Add(1); + _source1.Remove(1); + _results.Data.Count.Should().Be(0); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/EditDiffFixture.cs b/src/DynamicData.Tests/List/EditDiffFixture.cs index d0f26a302..24893b202 100644 --- a/src/DynamicData.Tests/List/EditDiffFixture.cs +++ b/src/DynamicData.Tests/List/EditDiffFixture.cs @@ -1,41 +1,46 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class EditDiffFixture: IDisposable + public class EditDiffFixture : IDisposable { private readonly SourceList _cache; + private readonly ChangeSetAggregator _result; - public EditDiffFixture() + public EditDiffFixture() { _cache = new SourceList(); _result = _cache.Connect().AsAggregator(); _cache.AddRange(Enumerable.Range(1, 10).Select(i => new Person("Name" + i, i)).ToArray()); } - public void Dispose() - { - _cache.Dispose(); - _result.Dispose(); - } - [Fact] - public void New() + public void Amends() { - var newPeople = Enumerable.Range(1, 15).Select(i => new Person("Name" + i, i)).ToArray(); + var newList = Enumerable.Range(5, 3).Select(i => new Person("Name" + i, i + 10)).ToArray(); + _cache.EditDiff(newList, Person.NameAgeGenderComparer); - _cache.EditDiff(newPeople, Person.NameAgeGenderComparer); + _cache.Count.Should().Be(3); - _cache.Count.Should().Be(15); - _cache.Items.Should().BeEquivalentTo(newPeople); var lastChange = _result.Messages.Last(); - lastChange.Adds.Should().Be(5); + lastChange.Adds.Should().Be(3); + lastChange.Removes.Should().Be(10); + + _cache.Items.Should().BeEquivalentTo(newList); + } + + public void Dispose() + { + _cache.Dispose(); + _result.Dispose(); } [Fact] @@ -51,18 +56,16 @@ public void EditWithSameData() } [Fact] - public void Amends() + public void New() { - var newList = Enumerable.Range(5, 3).Select(i => new Person("Name" + i, i + 10)).ToArray(); - _cache.EditDiff(newList, Person.NameAgeGenderComparer); + var newPeople = Enumerable.Range(1, 15).Select(i => new Person("Name" + i, i)).ToArray(); - _cache.Count.Should().Be(3); + _cache.EditDiff(newPeople, Person.NameAgeGenderComparer); + _cache.Count.Should().Be(15); + _cache.Items.Should().BeEquivalentTo(newPeople); var lastChange = _result.Messages.Last(); - lastChange.Adds.Should().Be(3); - lastChange.Removes.Should().Be(10); - - _cache.Items.Should().BeEquivalentTo(newList); + lastChange.Adds.Should().Be(5); } [Fact] @@ -96,4 +99,4 @@ public void VariousChanges() _cache.Items.Should().BeEquivalentTo(newList); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/ExceptFixture.cs b/src/DynamicData.Tests/List/ExceptFixture.cs index 859345a9a..b0dca30c2 100644 --- a/src/DynamicData.Tests/List/ExceptFixture.cs +++ b/src/DynamicData.Tests/List/ExceptFixture.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class ExceptFixture : ExceptFixtureBase { protected override IObservable> CreateObservable() @@ -24,10 +25,12 @@ protected override IObservable> CreateObservable() } } - public abstract class ExceptFixtureBase :IDisposable + public abstract class ExceptFixtureBase : IDisposable { protected ISourceList Source1; + protected ISourceList Source2; + private readonly ChangeSetAggregator _results; protected ExceptFixtureBase() @@ -37,44 +40,33 @@ protected ExceptFixtureBase() _results = CreateObservable().AsAggregator(); } - protected abstract IObservable> CreateObservable(); - - public void Dispose() - { - Source1.Dispose(); - Source2.Dispose(); - _results.Dispose(); - } - [Fact] - public void IncludedWhenItemIsInOneSource() + public void AddedWhenNoLongerInSecond() { Source1.Add(1); + Source2.Add(1); + Source2.Remove(1); _results.Data.Count.Should().Be(1); } [Fact] - public void NothingFromOther() + public void ClearFirstClearsResult() { - Source2.Add(1); + Source1.AddRange(Enumerable.Range(1, 5)); + Source2.AddRange(Enumerable.Range(1, 5)); + Source1.Clear(); _results.Data.Count.Should().Be(0); } [Fact] - public void ExcludedWhenItemIsInTwoSources() + public void ClearSecondEnsuresFirstIsIncluded() { - Source1.Add(1); - Source2.Add(1); + Source1.AddRange(Enumerable.Range(1, 5)); + Source2.AddRange(Enumerable.Range(1, 5)); _results.Data.Count.Should().Be(0); - } - - [Fact] - public void AddedWhenNoLongerInSecond() - { - Source1.Add(1); - Source2.Add(1); - Source2.Remove(1); - _results.Data.Count.Should().Be(1); + Source2.Clear(); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 5)); } [Fact] @@ -86,24 +78,35 @@ public void CombineRange() _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 5)); } + public void Dispose() + { + Source1.Dispose(); + Source2.Dispose(); + _results.Dispose(); + } + [Fact] - public void ClearFirstClearsResult() + public void ExcludedWhenItemIsInTwoSources() { - Source1.AddRange(Enumerable.Range(1, 5)); - Source2.AddRange(Enumerable.Range(1, 5)); - Source1.Clear(); + Source1.Add(1); + Source2.Add(1); _results.Data.Count.Should().Be(0); } [Fact] - public void ClearSecondEnsuresFirstIsIncluded() + public void IncludedWhenItemIsInOneSource() { - Source1.AddRange(Enumerable.Range(1, 5)); - Source2.AddRange(Enumerable.Range(1, 5)); + Source1.Add(1); + _results.Data.Count.Should().Be(1); + } + + [Fact] + public void NothingFromOther() + { + Source2.Add(1); _results.Data.Count.Should().Be(0); - Source2.Clear(); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 5)); } + + protected abstract IObservable> CreateObservable(); } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/ExpireAfterFixture.cs b/src/DynamicData.Tests/List/ExpireAfterFixture.cs index 8523cfee4..21df8b1bc 100644 --- a/src/DynamicData.Tests/List/ExpireAfterFixture.cs +++ b/src/DynamicData.Tests/List/ExpireAfterFixture.cs @@ -1,20 +1,24 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + +using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; -using FluentAssertions; namespace DynamicData.Tests.List { - - public class ExpireAfterFixture: IDisposable + public class ExpireAfterFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; private readonly TestScheduler _scheduler; + private readonly ISourceList _source; + public ExpireAfterFixture() { _scheduler = new TestScheduler(); @@ -22,10 +26,21 @@ public ExpireAfterFixture() _results = _source.Connect().AsAggregator(); } - public void Dispose() + [Fact] + public void CanHandleABatchOfUpdates() { - _results.Dispose(); - _source.Dispose(); + var remover = _source.ExpireAfter(p => TimeSpan.FromMilliseconds(100), _scheduler).Subscribe(); + const int size = 100; + Person[] items = Enumerable.Range(1, size).Select(i => new Person($"Name.{i}", i)).ToArray(); + + _source.AddRange(items); + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(200).Ticks); + remover.Dispose(); + + _results.Data.Count.Should().Be(0, "Should be no data in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(100, "Should be 100 adds in the first message"); + _results.Messages[1].Removes.Should().Be(100, "Should be 100 removes in the second message"); } [Fact] @@ -61,19 +76,10 @@ public void ComplexRemove() remover.Dispose(); } - [Fact] - public void ItemAddedIsExpired() + public void Dispose() { - var remover = _source.ExpireAfter(p => TimeSpan.FromMilliseconds(100), _scheduler).Subscribe(); - - _source.Add(new Person("Name1", 10)); - - _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(200).Ticks); - remover.Dispose(); - - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds in the first update"); - _results.Messages[1].Removes.Should().Be(1, "Should be 1 removes in the second update"); + _results.Dispose(); + _source.Dispose(); } [Fact] @@ -98,20 +104,18 @@ public void ExpireIsCancelledWhenUpdated() } [Fact] - public void CanHandleABatchOfUpdates() + public void ItemAddedIsExpired() { var remover = _source.ExpireAfter(p => TimeSpan.FromMilliseconds(100), _scheduler).Subscribe(); - const int size = 100; - Person[] items = Enumerable.Range(1, size).Select(i => new Person($"Name.{i}", i)).ToArray(); - _source.AddRange(items); + _source.Add(new Person("Name1", 10)); + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(200).Ticks); remover.Dispose(); - _results.Data.Count.Should().Be(0, "Should be no data in the cache"); _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should be 100 adds in the first message"); - _results.Messages[1].Removes.Should().Be(100, "Should be 100 removes in the second message"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds in the first update"); + _results.Messages[1].Removes.Should().Be(1, "Should be 1 removes in the second update"); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/FilterControllerFixtureWithClearAndReplace.cs b/src/DynamicData.Tests/List/FilterControllerFixtureWithClearAndReplace.cs index 044f893c4..6fa61e218 100644 --- a/src/DynamicData.Tests/List/FilterControllerFixtureWithClearAndReplace.cs +++ b/src/DynamicData.Tests/List/FilterControllerFixtureWithClearAndReplace.cs @@ -1,94 +1,30 @@ using System; using System.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class FilterControllerFixtureWithClearAndReplace : IDisposable + public class FilterControllerFixtureWithClearAndReplace : IDisposable { - private readonly ISourceList _source; + private readonly ISubject> _filter; + private readonly ChangeSetAggregator _results; - private readonly ISubject> _filter; - public FilterControllerFixtureWithClearAndReplace() + private readonly ISourceList _source; + + public FilterControllerFixtureWithClearAndReplace() { _source = new SourceList(); _filter = new BehaviorSubject>(p => p.Age > 20); - _results = _source.Connect().Filter(_filter,ListFilterPolicy.ClearAndReplace).AsAggregator(); - } - - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - - [Fact] - public void ChangeFilter() - { - var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); - - _source.AddRange(people); - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - - _filter.OnNext(p => p.Age <= 50); - _results.Data.Count.Should().Be(50, "Should be 50 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - - _results.Data.Items.All(p => p.Age <= 50).Should().BeTrue(); - } - - [Fact] - public void VeryLargeDataSet() - { - var filter = new BehaviorSubject>(i => false); - var source = new SourceList(); - - var result = source.Connect().Filter(filter, ListFilterPolicy.ClearAndReplace).AsObservableList(); - source.AddRange(Enumerable.Range(1,250000)); - - filter.OnNext(i => true); - filter.OnNext(i => false); + _results = _source.Connect().Filter(_filter, ListFilterPolicy.ClearAndReplace).AsAggregator(); } - [Fact] - public void ReevaluateFilter() - { - //re-evaluate for inline changes - var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); - - _source.AddRange(people); - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - - foreach (var person in people) - { - person.Age = person.Age + 10; - } - - _filter.OnNext(p => p.Age > 20); - - _results.Data.Count.Should().Be(90); - _results.Messages.Count.Should().Be(2); - _results.Messages[1].Removes.Should().Be(80); - _results.Messages[1].Adds.Should().Be(90); - - foreach (var person in people) - { - person.Age = person.Age - 10; - } - - _filter.OnNext(p => p.Age > 20); - - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - _results.Messages.Count.Should().Be(3, "Should be 3 update messages"); - } - - #region Static filter tests - /* Should be the same as standard lambda filter */ [Fact] @@ -119,11 +55,12 @@ public void AddNotMatchedAndUpdateMatched() var notmatched = new Person(key, 19); var matched = new Person(key, 21); - _source.Edit(updater => - { - updater.Add(notmatched); - updater.Add(matched); - }); + _source.Edit( + updater => + { + updater.Add(notmatched); + updater.Add(matched); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].First().Range.First().Should().Be(matched, "Should be same person"); @@ -180,6 +117,21 @@ public void BatchSuccessiveUpdates() _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(filtered, "Incorrect Filter result"); } + [Fact] + public void ChangeFilter() + { + var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); + + _source.AddRange(people); + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + + _filter.OnNext(p => p.Age <= 50); + _results.Data.Count.Should().Be(50, "Should be 50 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + + _results.Data.Items.All(p => p.Age <= 50).Should().BeTrue(); + } + [Fact] public void Clear() { @@ -193,6 +145,44 @@ public void Clear() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void ReevaluateFilter() + { + //re-evaluate for inline changes + var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); + + _source.AddRange(people); + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + + foreach (var person in people) + { + person.Age += 10; + } + + _filter.OnNext(p => p.Age > 20); + + _results.Data.Count.Should().Be(90); + _results.Messages.Count.Should().Be(2); + _results.Messages[1].Removes.Should().Be(80); + _results.Messages[1].Adds.Should().Be(90); + + foreach (var person in people) + { + person.Age -= 10; + } + + _filter.OnNext(p => p.Age > 20); + + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + _results.Messages.Count.Should().Be(3, "Should be 3 update messages"); + } + [Fact] public void Remove() { @@ -209,6 +199,24 @@ public void Remove() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + [Fact] + public void SameKeyChanges() + { + const string key = "Adult1"; + + _source.Edit( + updater => + { + updater.Add(new Person(key, 50)); + updater.Add(new Person(key, 52)); + updater.Add(new Person(key, 53)); + // updater.Remove(key); + }); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Messages[0].Adds.Should().Be(3, "Should be 3 adds"); + } + [Fact] public void UpdateMatched() { @@ -224,23 +232,6 @@ public void UpdateMatched() _results.Messages[1].Replaced.Should().Be(1, "Should be 1 update"); } - [Fact] - public void SameKeyChanges() - { - const string key = "Adult1"; - - _source.Edit(updater => - { - updater.Add(new Person(key, 50)); - updater.Add(new Person(key, 52)); - updater.Add(new Person(key, 53)); - // updater.Remove(key); - }); - - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Messages[0].Adds.Should().Be(3, "Should be 3 adds"); - } - [Fact] public void UpdateNotMatched() { @@ -255,6 +246,17 @@ public void UpdateNotMatched() _results.Data.Count.Should().Be(0, "Should nothing cached"); } - #endregion + [Fact] + public void VeryLargeDataSet() + { + var filter = new BehaviorSubject>(i => false); + var source = new SourceList(); + + var result = source.Connect().Filter(filter, ListFilterPolicy.ClearAndReplace).AsObservableList(); + source.AddRange(Enumerable.Range(1, 250000)); + + filter.OnNext(i => true); + filter.OnNext(i => false); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/FilterControllerFixtureWithDiffSet.cs b/src/DynamicData.Tests/List/FilterControllerFixtureWithDiffSet.cs index 3b06b98f4..8b8b06197 100644 --- a/src/DynamicData.Tests/List/FilterControllerFixtureWithDiffSet.cs +++ b/src/DynamicData.Tests/List/FilterControllerFixtureWithDiffSet.cs @@ -1,80 +1,30 @@ using System; using System.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - public class FilterControllerFixtureWithDiffSet : IDisposable { - private readonly ISourceList _source; - private readonly ChangeSetAggregator _results; private readonly ISubject> _filter; - public FilterControllerFixtureWithDiffSet() + private readonly ChangeSetAggregator _results; + + private readonly ISourceList _source; + + public FilterControllerFixtureWithDiffSet() { _source = new SourceList(); _filter = new BehaviorSubject>(p => p.Age > 20); _results = _source.Connect().Filter(_filter).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - - [Fact] - public void ChangeFilter() - { - var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); - - _source.AddRange(people); - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - - _filter.OnNext(p => p.Age <= 50); - _results.Data.Count.Should().Be(50, "Should be 50 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - - _results.Data.Items.All(p => p.Age <= 50).Should().BeTrue(); - } - - [Fact] - public void ReevaluateFilter() - { - //re-evaluate for inline changes - var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); - - _source.AddRange(people); - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - - foreach (var person in people) - { - person.Age = person.Age + 10; - } - - _filter.OnNext(p => p.Age > 20); - - _results.Data.Count.Should().Be(90, "Should be 90 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - _results.Messages[1].Adds.Should().Be(10, "Should be 10 adds in the second message"); - - foreach (var person in people) - { - person.Age = person.Age - 10; - } - - _filter.OnNext(p => p.Age > 20); - - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - _results.Messages.Count.Should().Be(3, "Should be 3 update messages"); - } - - #region Static filter tests - /* Should be the same as standard lambda filter */ [Fact] @@ -105,11 +55,12 @@ public void AddNotMatchedAndUpdateMatched() var notmatched = new Person(key, 19); var matched = new Person(key, 21); - _source.Edit(updater => - { - updater.Add(notmatched); - updater.Add(matched); - }); + _source.Edit( + updater => + { + updater.Add(notmatched); + updater.Add(matched); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].First().Range.First().Should().Be(matched, "Should be same person"); @@ -166,6 +117,21 @@ public void BatchSuccessiveUpdates() _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(filtered, "Incorrect Filter result"); } + [Fact] + public void ChangeFilter() + { + var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); + + _source.AddRange(people); + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + + _filter.OnNext(p => p.Age <= 50); + _results.Data.Count.Should().Be(50, "Should be 50 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + + _results.Data.Items.All(p => p.Age <= 50).Should().BeTrue(); + } + [Fact] public void Clear() { @@ -179,6 +145,43 @@ public void Clear() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void ReevaluateFilter() + { + //re-evaluate for inline changes + var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); + + _source.AddRange(people); + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + + foreach (var person in people) + { + person.Age += 10; + } + + _filter.OnNext(p => p.Age > 20); + + _results.Data.Count.Should().Be(90, "Should be 90 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + _results.Messages[1].Adds.Should().Be(10, "Should be 10 adds in the second message"); + + foreach (var person in people) + { + person.Age -= 10; + } + + _filter.OnNext(p => p.Age > 20); + + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + _results.Messages.Count.Should().Be(3, "Should be 3 update messages"); + } + [Fact] public void Remove() { @@ -195,6 +198,24 @@ public void Remove() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + [Fact] + public void SameKeyChanges() + { + const string key = "Adult1"; + + _source.Edit( + updater => + { + updater.Add(new Person(key, 50)); + updater.Add(new Person(key, 52)); + updater.Add(new Person(key, 53)); + // updater.Remove(key); + }); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Messages[0].Adds.Should().Be(3, "Should be 3 adds"); + } + [Fact] public void UpdateMatched() { @@ -210,23 +231,6 @@ public void UpdateMatched() _results.Messages[1].Replaced.Should().Be(1, "Should be 1 update"); } - [Fact] - public void SameKeyChanges() - { - const string key = "Adult1"; - - _source.Edit(updater => - { - updater.Add(new Person(key, 50)); - updater.Add(new Person(key, 52)); - updater.Add(new Person(key, 53)); - // updater.Remove(key); - }); - - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Messages[0].Adds.Should().Be(3, "Should be 3 adds"); - } - [Fact] public void UpdateNotMatched() { @@ -240,7 +244,5 @@ public void UpdateNotMatched() _results.Messages.Count.Should().Be(0, "Should be no updates"); _results.Data.Count.Should().Be(0, "Should nothing cached"); } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/FilterFixture.cs b/src/DynamicData.Tests/List/FilterFixture.cs index 8c1a59378..d918b4eaa 100644 --- a/src/DynamicData.Tests/List/FilterFixture.cs +++ b/src/DynamicData.Tests/List/FilterFixture.cs @@ -1,29 +1,26 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class FilterFixture: IDisposable + public class FilterFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public FilterFixture() + private readonly ISourceList _source; + + public FilterFixture() { _source = new SourceList(); _results = _source.Connect(p => p.Age > 20).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void AddMatched() { @@ -36,25 +33,32 @@ public void AddMatched() } [Fact] - public void ReplaceWithMatch() + public void AddNotMatched() { - var itemstoadd = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); - _source.AddRange(itemstoadd); - - _source.ReplaceAt(0, new Person("Adult1", 50)); + var person = new Person("Adult1", 10); + _source.Add(person); - _results.Data.Count.Should().Be(81); + _results.Messages.Count.Should().Be(0, "Should have no item updates"); + _results.Data.Count.Should().Be(0, "Cache should have no items"); } [Fact] - public void ReplaceWithNonMatch() + public void AddNotMatchedAndUpdateMatched() { - var itemstoadd = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); - _source.AddRange(itemstoadd); + const string key = "Adult1"; + var notmatched = new Person(key, 19); + var matched = new Person(key, 21); - _source.ReplaceAt(50, new Person("Adult1", 1)); + _source.Edit( + list => + { + list.Add(notmatched); + list.Add(matched); + }); - _results.Data.Count.Should().Be(79); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Messages[0].First().Range.First().Should().Be(matched, "Should be same person"); + _results.Data.Items.First().Should().Be(matched, "Should be same person"); } [Fact] @@ -70,45 +74,16 @@ public void AddRange() } [Fact] - public void Clear() - { - var itemstoadd = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); - - _source.AddRange(itemstoadd); - _source.Clear(); - - _results.Messages.Count.Should().Be(2, "Should be 1 updates"); - _results.Messages[0].First().Reason.Should().Be(ListChangeReason.AddRange, "First reason should be add range"); - _results.Messages[1].First().Reason.Should().Be(ListChangeReason.Clear, "Second reason should be clear"); - _results.Data.Count.Should().Be(0, "Should be 50 item in the cache"); - } - - [Fact] - public void AddNotMatched() - { - var person = new Person("Adult1", 10); - _source.Add(person); - - _results.Messages.Count.Should().Be(0, "Should have no item updates"); - _results.Data.Count.Should().Be(0, "Cache should have no items"); - } - - [Fact] - public void AddNotMatchedAndUpdateMatched() + public void AddSubscribeRemove() { - const string key = "Adult1"; - var notmatched = new Person(key, 19); - var matched = new Person(key, 21); + var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); + var source = new SourceList(); + source.AddRange(people); - _source.Edit(list => - { - list.Add(notmatched); - list.Add(matched); - }); + var results = source.Connect(x => x.Age > 20).AsAggregator(); + source.RemoveMany(people.Where(x => x.Age % 2 == 0)); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Messages[0].First().Range.First().Should().Be(matched, "Should be same person"); - _results.Data.Items.First().Should().Be(matched, "Should be same person"); + results.Data.Count.Should().Be(40, "Should be 40 cached"); } [Fact] @@ -160,6 +135,20 @@ public void BatchSuccessiveUpdates() _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(filtered, "Incorrect Filter result"); } + [Fact] + public void Clear() + { + var itemstoadd = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); + + _source.AddRange(itemstoadd); + _source.Clear(); + + _results.Messages.Count.Should().Be(2, "Should be 1 updates"); + _results.Messages[0].First().Reason.Should().Be(ListChangeReason.AddRange, "First reason should be add range"); + _results.Messages[1].First().Reason.Should().Be(ListChangeReason.Clear, "Second reason should be clear"); + _results.Data.Count.Should().Be(0, "Should be 50 item in the cache"); + } + [Fact] public void Clear1() { @@ -173,6 +162,12 @@ public void Clear1() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + [Fact] public void Remove() { @@ -189,19 +184,42 @@ public void Remove() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + [Fact] + public void ReplaceWithMatch() + { + var itemstoadd = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); + _source.AddRange(itemstoadd); + + _source.ReplaceAt(0, new Person("Adult1", 50)); + + _results.Data.Count.Should().Be(81); + } + + [Fact] + public void ReplaceWithNonMatch() + { + var itemstoadd = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); + _source.AddRange(itemstoadd); + + _source.ReplaceAt(50, new Person("Adult1", 1)); + + _results.Data.Count.Should().Be(79); + } + [Fact] public void SameKeyChanges() { const string key = "Adult1"; var toaddandremove = new Person(key, 53); - _source.Edit(updater => - { - updater.Add(new Person(key, 50)); - updater.Add(new Person(key, 52)); - updater.Add(toaddandremove); - updater.Remove(toaddandremove); - }); + _source.Edit( + updater => + { + updater.Add(new Person(key, 50)); + updater.Add(new Person(key, 52)); + updater.Add(toaddandremove); + updater.Remove(toaddandremove); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].Adds.Should().Be(3, "Should be 3 adds"); @@ -222,19 +240,5 @@ public void UpdateNotMatched() _results.Messages.Count.Should().Be(0, "Should be no updates"); _results.Data.Count.Should().Be(0, "Should nothing cached"); } - - [Fact] - public void AddSubscribeRemove() - { - var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - var source = new SourceList(); - source.AddRange(people); - - var results = source.Connect(x => x.Age > 20).AsAggregator(); - source.RemoveMany(people.Where(x => x.Age % 2 == 0)); - - results.Data.Count.Should().Be(40, "Should be 40 cached"); - } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/FilterOnObservableFixture.cs b/src/DynamicData.Tests/List/FilterOnObservableFixture.cs index 69d1288f9..284b33b52 100644 --- a/src/DynamicData.Tests/List/FilterOnObservableFixture.cs +++ b/src/DynamicData.Tests/List/FilterOnObservableFixture.cs @@ -1,8 +1,11 @@ using System; using System.Linq; using System.Reactive.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List @@ -12,118 +15,106 @@ namespace DynamicData.Tests.List public class FilterOnObservableFixture { [Fact] - public void InitialValues() + public void ChangeAValueSoItIsStillInTheFilter() { - var people = Enumerable.Range(1, 100).Select(i => new PersonObs ("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); - - // should have 100-18 left - stub.Results.Data.Count.Should().Be(82); - - // initial addrange, refreshes to filter out < 18 - // stub.Results.Messages.Count.Should().Be(1+18); - - stub.Results.Data.Items.Should().BeEquivalentTo(people.Skip(18)); - } + var people = Enumerable.Range(1, 100).Select(i => new PersonObs("Name" + i, i)).ToArray(); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); + + people[50].SetAge(100); + stub.Results.Data.Count.Should().Be(82); + // initial add range, refreshes to filter out < 18 and then no refresh for the no-op filter change + // stub.Results.Messages.Count.Should().Be(102); } [Fact] public void ChangeAValueToMatchFilter() { - var people = Enumerable.Range(1, 100).Select(i => new PersonObs ("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); + var people = Enumerable.Range(1, 100).Select(i => new PersonObs("Name" + i, i)).ToArray(); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); - people[20].SetAge(10); + people[20].SetAge(10); - // should have 100-18-1 left - stub.Results.Data.Count.Should().Be(81); + // should have 100-18-1 left + stub.Results.Data.Count.Should().Be(81); - // initial addrange, refreshes to filter out < 18 and then refresh for the filter change -// stub.Results.Messages.Count.Should().Be(1+18+1); - } + // initial addrange, refreshes to filter out < 18 and then refresh for the filter change + // stub.Results.Messages.Count.Should().Be(1+18+1); } [Fact] public void ChangeAValueToNoLongerMatchFilter() { - var people = Enumerable.Range(1, 100).Select(i => new PersonObs ("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); + var people = Enumerable.Range(1, 100).Select(i => new PersonObs("Name" + i, i)).ToArray(); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); - // should have 100-18 left - stub.Results.Data.Count.Should().Be(82); + // should have 100-18 left + stub.Results.Data.Count.Should().Be(82); - // stub.Results.Messages.Count.Should().Be(1+18); + // stub.Results.Messages.Count.Should().Be(1+18); - people[10].SetAge(20); + people[10].SetAge(20); - // should have 82+1 left - stub.Results.Data.Count.Should().Be(83); + // should have 82+1 left + stub.Results.Data.Count.Should().Be(83); - // initial addrange, refreshes to filter out < 18 and then one refresh for the filter change - // stub.Results.Messages.Count.Should().Be(1+18+1); - } + // initial addrange, refreshes to filter out < 18 and then one refresh for the filter change + // stub.Results.Messages.Count.Should().Be(1+18+1); } [Fact] - public void ChangeAValueSoItIsStillInTheFilter() + public void Clear() { - var people = Enumerable.Range(1, 100).Select(i => new PersonObs ("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); + var people = Enumerable.Range(1, 100).Select(i => new PersonObs("Name" + i, i)).ToArray(); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); + stub.Source.Clear(); - people[50].SetAge(100); - stub.Results.Data.Count.Should().Be(82); - // initial add range, refreshes to filter out < 18 and then no refresh for the no-op filter change - // stub.Results.Messages.Count.Should().Be(102); - } + stub.Results.Data.Count.Should().Be(0); } [Fact] - public void Clear() + public void InitialValues() { - var people = Enumerable.Range(1, 100).Select(i => new PersonObs ("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); - stub.Source.Clear(); + var people = Enumerable.Range(1, 100).Select(i => new PersonObs("Name" + i, i)).ToArray(); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); - stub.Results.Data.Count.Should().Be(0); - } + // should have 100-18 left + stub.Results.Data.Count.Should().Be(82); + + // initial addrange, refreshes to filter out < 18 + // stub.Results.Messages.Count.Should().Be(1+18); + + stub.Results.Data.Items.Should().BeEquivalentTo(people.Skip(18)); } [Fact] public void RemoveRange() { - var people = Enumerable.Range(1, 100).Select(i => new PersonObs ("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); - stub.Source.RemoveRange(89,10); + var people = Enumerable.Range(1, 100).Select(i => new PersonObs("Name" + i, i)).ToArray(); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); + stub.Source.RemoveRange(89, 10); - stub.Results.Data.Count.Should().Be(72); - // initial addrange, refreshes to filter out < 18 and then removerange + stub.Results.Data.Count.Should().Be(72); + // initial addrange, refreshes to filter out < 18 and then removerange // stub.Results.Messages.Count.Should().Be(1+18+1); - } } private class FilterPropertyStub : IDisposable { - public ISourceList Source { get; } = new SourceList(); - public ChangeSetAggregator Results { get; } - public FilterPropertyStub() { - Results = new ChangeSetAggregator(Source.Connect() - .FilterOnObservable(p => p.Age.Select(v => v > 18))); + Results = new ChangeSetAggregator(Source.Connect().FilterOnObservable(p => p.Age.Select(v => v > 18))); } + public ChangeSetAggregator Results { get; } + + public ISourceList Source { get; } = new SourceList(); + public void Dispose() { Source.Dispose(); diff --git a/src/DynamicData.Tests/List/FilterOnPropertyFixture.cs b/src/DynamicData.Tests/List/FilterOnPropertyFixture.cs index e35f47714..8f397fedf 100644 --- a/src/DynamicData.Tests/List/FilterOnPropertyFixture.cs +++ b/src/DynamicData.Tests/List/FilterOnPropertyFixture.cs @@ -1,109 +1,100 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class FilterOnPropertyFixture { [Fact] - public void InitialValues() + public void ChangeAValueSoItIsStillInTheFilter() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); - - 1.Should().Be(stub.Results.Messages.Count); - 82.Should().Be(stub.Results.Data.Count); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); - stub.Results.Data.Items.Should().BeEquivalentTo(people.Skip(18)); - } + people[50].Age = 100; + stub.Results.Messages.Count.Should().Be(2); + stub.Results.Data.Count.Should().Be(82); } [Fact] public void ChangeAValueToMatchFilter() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); - people[20].Age = 10; + people[20].Age = 10; - 2.Should().Be(stub.Results.Messages.Count); - 81.Should().Be(stub.Results.Data.Count); - } + 2.Should().Be(stub.Results.Messages.Count); + 81.Should().Be(stub.Results.Data.Count); } [Fact] public void ChangeAValueToNoLongerMatchFilter() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); - people[10].Age = 20; + people[10].Age = 20; - 2.Should().Be(stub.Results.Messages.Count); - 83.Should().Be(stub.Results.Data.Count); - } + 2.Should().Be(stub.Results.Messages.Count); + 83.Should().Be(stub.Results.Data.Count); } [Fact] - public void ChangeAValueSoItIsStillInTheFilter() + public void Clear() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); + stub.Source.Clear(); - people[50].Age = 100; - stub.Results.Messages.Count.Should().Be(2); - stub.Results.Data.Count.Should().Be(82); - } + stub.Results.Data.Count.Should().Be(0); } [Fact] - public void Clear() + public void InitialValues() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); - stub.Source.Clear(); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); - stub.Results.Data.Count.Should().Be(0); - } + 1.Should().Be(stub.Results.Messages.Count); + 82.Should().Be(stub.Results.Data.Count); + + stub.Results.Data.Items.Should().BeEquivalentTo(people.Skip(18)); } [Fact] public void RemoveRange() { var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - using (var stub = new FilterPropertyStub()) - { - stub.Source.AddRange(people); - stub.Source.RemoveRange(89,10); + using var stub = new FilterPropertyStub(); + stub.Source.AddRange(people); + stub.Source.RemoveRange(89, 10); - stub.Results.Data.Count.Should().Be(72); - } + stub.Results.Data.Count.Should().Be(72); } private class FilterPropertyStub : IDisposable { - public ISourceList Source { get; } = new SourceList(); - public ChangeSetAggregator Results { get; } - public FilterPropertyStub() { Results = new ChangeSetAggregator(Source.Connect().FilterOnProperty(p => p.Age, p => p.Age > 18)); } + public ChangeSetAggregator Results { get; } + + public ISourceList Source { get; } = new SourceList(); + public void Dispose() { Source.Dispose(); diff --git a/src/DynamicData.Tests/List/FilterWithObservable.cs b/src/DynamicData.Tests/List/FilterWithObservable.cs index ab0d418c2..d57c12cb7 100644 --- a/src/DynamicData.Tests/List/FilterWithObservable.cs +++ b/src/DynamicData.Tests/List/FilterWithObservable.cs @@ -1,135 +1,32 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; + using DynamicData.Aggregation; using DynamicData.Tests.Domain; -using Xunit; -using System.Collections.Generic; using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class FilterWithObservable: IDisposable + public class FilterWithObservable : IDisposable { - private readonly ISourceList _source; - private readonly ChangeSetAggregator _results; private readonly BehaviorSubject> _filter; - public FilterWithObservable() + private readonly ChangeSetAggregator _results; + + private readonly ISourceList _source; + + public FilterWithObservable() { _source = new SourceList(); _filter = new BehaviorSubject>(p => p.Age > 20); _results = _source.Connect().Filter(_filter).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - - [Fact] - public void ChangeFilter() - { - var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); - - _source.AddRange(people); - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - - _filter.OnNext(p => p.Age <= 50); - _results.Data.Count.Should().Be(50, "Should be 50 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - - _results.Data.Items.All(p => p.Age <= 50).Should().BeTrue(); - } - - [Fact] - public void ReevaluateFilter() - { - //re-evaluate for inline changes - var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); - - _source.AddRange(people); - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - - foreach (var person in people) - { - person.Age = person.Age + 10; - } - - _filter.OnNext(_filter.Value); - - _results.Data.Count.Should().Be(90, "Should be 90 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - _results.Messages[1].Removes.Should().Be(0, "Should be 80 removes in the second message"); - _results.Messages[1].Adds.Should().Be(10, "Should be 10 adds in the second message"); - - foreach (var person in people) - { - person.Age = person.Age - 10; - } - - _filter.OnNext(_filter.Value); - - _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); - _results.Messages.Count.Should().Be(3, "Should be 3 update messages"); - } - - [Fact] - public void RemoveFiltered() - { - var person = new Person("P1", 1); - - _source.Add(person); - _results.Data.Count.Should().Be(0, "Should be 0 people in the cache"); - _filter.OnNext(p => p.Age >= 1); - _results.Data.Count.Should().Be(1, "Should be 1 people in the cache"); - - _source.Remove(person); - - _results.Data.Count.Should().Be(0, "Should be 0 people in the cache"); - } - - [Fact] - public void RemoveFilteredRange() - { - var people = Enumerable.Range(1, 10).Select(i => new Person("P" + i, i)).ToArray(); - - _source.AddRange(people); - _results.Data.Count.Should().Be(0, "Should be 0 people in the cache"); - _filter.OnNext(p => p.Age > 5); - _results.Data.Count.Should().Be(5, "Should be 5 people in the cache"); - - _source.RemoveRange(5, 5); - - _results.Data.Count.Should().Be(0, "Should be 0 people in the cache"); - } - - [Fact] - public void ChainFilters() - { - var filter2 = new BehaviorSubject>(person1 => person1.Age > 20); - - var stream = _source.Connect() - .Filter(_filter) - .Filter(filter2); - - var captureList = new List(); - stream.Count().Subscribe(count => captureList.Add(count)); - - var person = new Person("P", 30); - _source.Add(person); - - person.Age = 10; - _filter.OnNext(_filter.Value); - - captureList.Should().BeEquivalentTo(new[] {1, 0}); - } - - #region Static filter tests - /* Should be the same as standard lambda filter */ [Fact] @@ -160,11 +57,12 @@ public void AddNotMatchedAndUpdateMatched() var notmatched = new Person(key, 19); var matched = new Person(key, 21); - _source.Edit(updater => - { - updater.Add(notmatched); - updater.Add(matched); - }); + _source.Edit( + updater => + { + updater.Add(notmatched); + updater.Add(matched); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].First().Range.First().Should().Be(matched, "Should be same person"); @@ -221,6 +119,40 @@ public void BatchSuccessiveUpdates() _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(filtered, "Incorrect Filter result"); } + [Fact] + public void ChainFilters() + { + var filter2 = new BehaviorSubject>(person1 => person1.Age > 20); + + var stream = _source.Connect().Filter(_filter).Filter(filter2); + + var captureList = new List(); + stream.Count().Subscribe(count => captureList.Add(count)); + + var person = new Person("P", 30); + _source.Add(person); + + person.Age = 10; + _filter.OnNext(_filter.Value); + + captureList.Should().BeEquivalentTo(new[] { 1, 0 }); + } + + [Fact] + public void ChangeFilter() + { + var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToList(); + + _source.AddRange(people); + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + + _filter.OnNext(p => p.Age <= 50); + _results.Data.Count.Should().Be(50, "Should be 50 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + + _results.Data.Items.All(p => p.Age <= 50).Should().BeTrue(); + } + [Fact] public void Clear() { @@ -234,6 +166,44 @@ public void Clear() _results.Data.Count.Should().Be(0, "Should be nothing cached"); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void ReevaluateFilter() + { + //re-evaluate for inline changes + var people = Enumerable.Range(1, 100).Select(i => new Person("P" + i, i)).ToArray(); + + _source.AddRange(people); + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + + foreach (var person in people) + { + person.Age += 10; + } + + _filter.OnNext(_filter.Value); + + _results.Data.Count.Should().Be(90, "Should be 90 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + _results.Messages[1].Removes.Should().Be(0, "Should be 80 removes in the second message"); + _results.Messages[1].Adds.Should().Be(10, "Should be 10 adds in the second message"); + + foreach (var person in people) + { + person.Age -= 10; + } + + _filter.OnNext(_filter.Value); + + _results.Data.Count.Should().Be(80, "Should be 80 people in the cache"); + _results.Messages.Count.Should().Be(3, "Should be 3 update messages"); + } + [Fact] public void Remove() { @@ -251,18 +221,33 @@ public void Remove() } [Fact] - public void UpdateMatched() + public void RemoveFiltered() { - const string key = "Adult1"; - var newperson = new Person(key, 50); - var updated = new Person(key, 51); + var person = new Person("P1", 1); - _source.Add(newperson); - _source.Replace(newperson, updated); + _source.Add(person); + _results.Data.Count.Should().Be(0, "Should be 0 people in the cache"); + _filter.OnNext(p => p.Age >= 1); + _results.Data.Count.Should().Be(1, "Should be 1 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - _results.Messages[1].Replaced.Should().Be(1, "Should be 1 update"); + _source.Remove(person); + + _results.Data.Count.Should().Be(0, "Should be 0 people in the cache"); + } + + [Fact] + public void RemoveFilteredRange() + { + var people = Enumerable.Range(1, 10).Select(i => new Person("P" + i, i)).ToArray(); + + _source.AddRange(people); + _results.Data.Count.Should().Be(0, "Should be 0 people in the cache"); + _filter.OnNext(p => p.Age > 5); + _results.Data.Count.Should().Be(5, "Should be 5 people in the cache"); + + _source.RemoveRange(5, 5); + + _results.Data.Count.Should().Be(0, "Should be 0 people in the cache"); } [Fact] @@ -270,18 +255,34 @@ public void SameKeyChanges() { const string key = "Adult1"; - _source.Edit(updater => - { - updater.Add(new Person(key, 50)); - updater.Add(new Person(key, 52)); - updater.Add(new Person(key, 53)); - // updater.Remove(key); - }); + _source.Edit( + updater => + { + updater.Add(new Person(key, 50)); + updater.Add(new Person(key, 52)); + updater.Add(new Person(key, 53)); + // updater.Remove(key); + }); _results.Messages.Count.Should().Be(1, "Should be 1 updates"); _results.Messages[0].Adds.Should().Be(3, "Should be 3 adds"); } + [Fact] + public void UpdateMatched() + { + const string key = "Adult1"; + var newperson = new Person(key, 50); + var updated = new Person(key, 51); + + _source.Add(newperson); + _source.Replace(newperson, updated); + + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); + _results.Messages[1].Replaced.Should().Be(1, "Should be 1 update"); + } + [Fact] public void UpdateNotMatched() { @@ -295,7 +296,5 @@ public void UpdateNotMatched() _results.Messages.Count.Should().Be(0, "Should be no updates"); _results.Data.Count.Should().Be(0, "Should nothing cached"); } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/ForEachChangeFixture.cs b/src/DynamicData.Tests/List/ForEachChangeFixture.cs index ebbd69a62..049023fd4 100644 --- a/src/DynamicData.Tests/List/ForEachChangeFixture.cs +++ b/src/DynamicData.Tests/List/ForEachChangeFixture.cs @@ -1,18 +1,20 @@ using System; using System.Collections.Generic; + using DynamicData.Kernel; using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class ForEachChangeFixture: IDisposable + public class ForEachChangeFixture : IDisposable { private readonly ISourceList _source; - public ForEachChangeFixture() + public ForEachChangeFixture() { _source = new SourceList(); } @@ -27,10 +29,7 @@ public void EachChangeInokesTheCallback() { var messages = new List>(); - var messageWriter = _source - .Connect() - .ForEachChange(messages.Add) - .Subscribe(); + var messageWriter = _source.Connect().ForEachChange(messages.Add).Subscribe(); var people = new RandomPersonGenerator().Take(100); people.ForEach(_source.Add); @@ -40,34 +39,30 @@ public void EachChangeInokesTheCallback() } [Fact] - public void EachItemChangeInokesTheCallback() + public void EachItemChangeInokesTheCallbac2() { var messages = new List>(); - var messageWriter = _source.Connect() - .ForEachItemChange(messages.Add) - .Subscribe(); - - _source.AddRange(new RandomPersonGenerator().Take(100)); + var messageWriter = _source.Connect().ForEachItemChange(messages.Add).Subscribe(); + _source.AddRange(new RandomPersonGenerator().Take(5)); + _source.InsertRange(new RandomPersonGenerator().Take(5), 2); + _source.AddRange(new RandomPersonGenerator().Take(5)); - messages.Count.Should().Be(100); + messages.Count.Should().Be(15); messageWriter.Dispose(); } [Fact] - public void EachItemChangeInokesTheCallbac2() + public void EachItemChangeInokesTheCallback() { var messages = new List>(); - var messageWriter = _source.Connect() - .ForEachItemChange(messages.Add) - .Subscribe(); - _source.AddRange(new RandomPersonGenerator().Take(5)); - _source.InsertRange(new RandomPersonGenerator().Take(5), 2); - _source.AddRange(new RandomPersonGenerator().Take(5)); + var messageWriter = _source.Connect().ForEachItemChange(messages.Add).Subscribe(); - messages.Count.Should().Be(15); + _source.AddRange(new RandomPersonGenerator().Take(100)); + + messages.Count.Should().Be(100); messageWriter.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/FromAsyncFixture.cs b/src/DynamicData.Tests/List/FromAsyncFixture.cs index f0a52f835..cec0a1ecd 100644 --- a/src/DynamicData.Tests/List/FromAsyncFixture.cs +++ b/src/DynamicData.Tests/List/FromAsyncFixture.cs @@ -3,19 +3,22 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.List { - public class FromAsyncFixture { - private TestScheduler _scheduler; + private readonly TestScheduler _scheduler; - public FromAsyncFixture() + public FromAsyncFixture() { _scheduler = new TestScheduler(); } @@ -25,17 +28,12 @@ public void CanLoadFromTask() { Task> Loader() { - var items = Enumerable.Range(1, 100) - .Select(i => new Person("Person" + i, 1)) - .ToArray() - .AsEnumerable(); + var items = Enumerable.Range(1, 100).Select(i => new Person("Person" + i, 1)).ToArray().AsEnumerable(); return Task.FromResult(items); } - var data = Observable.FromAsync((Func>>) Loader) - .ToObservableChangeSet() - .AsObservableList(); + var data = Observable.FromAsync((Func>>)Loader).ToObservableChangeSet().AsObservableList(); data.Count.Should().Be(100); } @@ -49,11 +47,9 @@ Task> Loader() throw new Exception("Broken"); } - Exception error = null; + Exception? error = null; - var data = Observable.FromAsync((Func>>) Loader) - .ToObservableChangeSet() - .Subscribe((changes) => { }, ex => error = ex); + var data = Observable.FromAsync((Func>>)Loader).ToObservableChangeSet().Subscribe((changes) => { }, ex => error = ex); error.Should().NotBeNull(); } @@ -67,17 +63,13 @@ Task> Loader() throw new Exception("Broken"); } - Exception error = null; + Exception? error = null; - var data = Observable.FromAsync((Func>>) Loader) - .ToObservableChangeSet() - .AsObservableList(); + var data = Observable.FromAsync((Func>>)Loader).ToObservableChangeSet().AsObservableList(); - var subscribed = data.Connect() - .Subscribe(changes => { }, ex => error = ex); + var subscribed = data.Connect().Subscribe(changes => { }, ex => error = ex); error.Should().NotBeNull(); } - } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/GroupImmutableFixture.cs b/src/DynamicData.Tests/List/GroupImmutableFixture.cs index 74199f60e..258ced423 100644 --- a/src/DynamicData.Tests/List/GroupImmutableFixture.cs +++ b/src/DynamicData.Tests/List/GroupImmutableFixture.cs @@ -2,94 +2,84 @@ using System.Linq; using System.Reactive; using System.Reactive.Subjects; -using DynamicData.Tests.Domain; -using Xunit; + using DynamicData.Kernel; +using DynamicData.Tests.Domain; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class GroupImmutableFixture: IDisposable + public class GroupImmutableFixture : IDisposable { - private readonly ISourceList _source; - private readonly ChangeSetAggregator> _results; private readonly ISubject _regrouper; - public GroupImmutableFixture() + private readonly ChangeSetAggregator> _results; + + private readonly ISourceList _source; + + public GroupImmutableFixture() { _source = new SourceList(); _regrouper = new Subject(); _results = _source.Connect().GroupWithImmutableState(p => p.Age, _regrouper).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void Add() { - _source.Add(new Person("Person1", 20)); _results.Data.Count.Should().Be(1, "Should be 1 add"); _results.Messages.First().Adds.Should().Be(1); } [Fact] - public void UpdatesArePermissible() + public void ChanegMultipleGroups() { - _source.Add(new Person("Person1", 20)); - _source.Add(new Person("Person2", 20)); + var initialPeople = Enumerable.Range(1, 10000).Select(i => new Person("Person" + i, i % 10)).ToArray(); - _results.Data.Count.Should().Be(1);//1 group - _results.Messages.First().Adds.Should().Be(1); - _results.Messages.Skip(1).First().Replaced.Should().Be(1); + _source.AddRange(initialPeople); - var group = _results.Data.Items.First(); - group.Count.Should().Be(2); - } + initialPeople.GroupBy(p => p.Age).ForEach( + group => + { + var grp = _results.Data.Items.First(g => g.Key.Equals(group.Key)); + grp.Items.Should().BeEquivalentTo(group.ToArray()); + }); - [Fact] - public void UpdateAnItemWillChangedThegroup() - { - var person1 = new Person("Person1", 20); - _source.Add(person1); - _source.Replace(person1, new Person("Person1", 21)); + _source.RemoveMany(initialPeople.Take(15)); - _results.Data.Count.Should().Be(1); - _results.Messages.First().Adds.Should().Be(1); - _results.Messages.Skip(1).First().Adds.Should().Be(1); - _results.Messages.Skip(1).First().Removes.Should().Be(1); - var group = _results.Data.Items.First(); - group.Count.Should().Be(1); + initialPeople.Skip(15).GroupBy(p => p.Age).ForEach( + group => + { + var list = _results.Data.Items.First(p => p.Key == group.Key); + list.Items.Should().BeEquivalentTo(group); + }); - group.Key.Should().Be(21); + _results.Messages.Count.Should().Be(2); + _results.Messages.First().Adds.Should().Be(10); + _results.Messages.Skip(1).First().Replaced.Should().Be(10); } - [Fact] - public void Remove() + public void Dispose() { - var person = new Person("Person1", 20); - _source.Add(person); - _source.Remove(person); - - _results.Messages.Count.Should().Be(2); - _results.Data.Count.Should().Be(0); + _source.Dispose(); + _results.Dispose(); } [Fact] public void FiresManyValueForBatchOfDifferentAdds() { - _source.Edit(updater => - { - updater.Add(new Person("Person1", 20)); - updater.Add(new Person("Person2", 21)); - updater.Add(new Person("Person3", 22)); - updater.Add(new Person("Person4", 23)); - }); + _source.Edit( + updater => + { + updater.Add(new Person("Person1", 20)); + updater.Add(new Person("Person2", 21)); + updater.Add(new Person("Person3", 22)); + updater.Add(new Person("Person4", 23)); + }); _results.Data.Count.Should().Be(4); _results.Messages.Count.Should().Be(1); @@ -103,56 +93,24 @@ public void FiresManyValueForBatchOfDifferentAdds() [Fact] public void FiresOnlyOnceForABatchOfUniqueValues() { - _source.Edit(updater => - { - updater.Add(new Person("Person1", 20)); - updater.Add(new Person("Person2", 20)); - updater.Add(new Person("Person3", 20)); - updater.Add(new Person("Person4", 20)); - }); + _source.Edit( + updater => + { + updater.Add(new Person("Person1", 20)); + updater.Add(new Person("Person2", 20)); + updater.Add(new Person("Person3", 20)); + updater.Add(new Person("Person4", 20)); + }); _results.Messages.Count.Should().Be(1); _results.Messages.First().Adds.Should().Be(1); _results.Data.Items.First().Count.Should().Be(4); } - [Fact] - public void ChanegMultipleGroups() - { - var initialPeople = Enumerable.Range(1, 10000) - .Select(i => new Person("Person" + i, i % 10)) - .ToArray(); - - _source.AddRange(initialPeople); - - initialPeople.GroupBy(p => p.Age) - .ForEach(group => - { - var grp = _results.Data.Items.First(g=> g.Key.Equals(group.Key)); - grp.Items.Should().BeEquivalentTo(group.ToArray()); - }); - - _source.RemoveMany(initialPeople.Take(15)); - - initialPeople.Skip(15) - .GroupBy(p => p.Age) - .ForEach(group => - { - var list = _results.Data.Items.First(p => p.Key == group.Key); - list.Items.Should().BeEquivalentTo(group); - }); - - _results.Messages.Count.Should().Be(2); - _results.Messages.First().Adds.Should().Be(10); - _results.Messages.Skip(1).First().Replaced.Should().Be(10); - } - [Fact] public void Reevaluate() { - var initialPeople = Enumerable.Range(1, 10) - .Select(i => new Person("Person" + i, i % 2)) - .ToArray(); + var initialPeople = Enumerable.Range(1, 10).Select(i => new Person("Person" + i, i % 2)).ToArray(); _source.AddRange(initialPeople); _results.Messages.Count.Should().Be(1); @@ -160,19 +118,18 @@ public void Reevaluate() //do an inline update foreach (var person in initialPeople) { - person.Age = person.Age + 1; + person.Age += 1; } //signal operators to evaluate again _regrouper.OnNext(); - initialPeople.GroupBy(p => p.Age) - .ForEach(groupContainer => - { - var grouping = _results.Data.Items.First(g => g.Key == groupContainer.Key); - grouping.Items.Should().BeEquivalentTo(groupContainer); - - }); + initialPeople.GroupBy(p => p.Age).ForEach( + groupContainer => + { + var grouping = _results.Data.Items.First(g => g.Key == groupContainer.Key); + grouping.Items.Should().BeEquivalentTo(groupContainer); + }); _results.Data.Count.Should().Be(2); _results.Messages.Count.Should().Be(2); @@ -182,5 +139,47 @@ public void Reevaluate() secondMessage.Replaced.Should().Be(1); secondMessage.Adds.Should().Be(1); } + + [Fact] + public void Remove() + { + var person = new Person("Person1", 20); + _source.Add(person); + _source.Remove(person); + + _results.Messages.Count.Should().Be(2); + _results.Data.Count.Should().Be(0); + } + + [Fact] + public void UpdateAnItemWillChangedThegroup() + { + var person1 = new Person("Person1", 20); + _source.Add(person1); + _source.Replace(person1, new Person("Person1", 21)); + + _results.Data.Count.Should().Be(1); + _results.Messages.First().Adds.Should().Be(1); + _results.Messages.Skip(1).First().Adds.Should().Be(1); + _results.Messages.Skip(1).First().Removes.Should().Be(1); + var group = _results.Data.Items.First(); + group.Count.Should().Be(1); + + group.Key.Should().Be(21); + } + + [Fact] + public void UpdatesArePermissible() + { + _source.Add(new Person("Person1", 20)); + _source.Add(new Person("Person2", 20)); + + _results.Data.Count.Should().Be(1); //1 group + _results.Messages.First().Adds.Should().Be(1); + _results.Messages.Skip(1).First().Replaced.Should().Be(1); + + var group = _results.Data.Items.First(); + group.Count.Should().Be(2); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/GroupOnFixture.cs b/src/DynamicData.Tests/List/GroupOnFixture.cs index 2373ea139..793a420e4 100644 --- a/src/DynamicData.Tests/List/GroupOnFixture.cs +++ b/src/DynamicData.Tests/List/GroupOnFixture.cs @@ -1,28 +1,26 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class GroupOnFixture: IDisposable + public class GroupOnFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator> _results; - public GroupOnFixture() + private readonly ISourceList _source; + + public GroupOnFixture() { _source = new SourceList(); _results = _source.Connect().GroupOn(p => p.Age).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - } - [Fact] public void Add() { @@ -36,6 +34,21 @@ public void Add() firstGroup[0].Should().Be(person, "Should be same person"); } + [Fact] + public void BigList() + { + var generator = new RandomPersonGenerator(); + var people = generator.Take(10000).ToArray(); + _source.AddRange(people); + + Console.WriteLine(); + } + + public void Dispose() + { + _source.Dispose(); + } + [Fact] public void Remove() { @@ -60,15 +73,5 @@ public void UpdateWillChangeTheGroup() var firstGroup = _results.Data.Items.First().List.Items.ToArray(); firstGroup[0].Should().Be(amended, "Should be same person"); } - - [Fact] - public void BigList() - { - var generator = new RandomPersonGenerator(); - var people = generator.Take(10000).ToArray(); - _source.AddRange(people); - - Console.WriteLine(); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/GroupOnPropertyFixture.cs b/src/DynamicData.Tests/List/GroupOnPropertyFixture.cs index 183805e3d..4e00703bc 100644 --- a/src/DynamicData.Tests/List/GroupOnPropertyFixture.cs +++ b/src/DynamicData.Tests/List/GroupOnPropertyFixture.cs @@ -1,65 +1,38 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class GroupOnPropertyFixture: IDisposable + public class GroupOnPropertyFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator> _results; - public GroupOnPropertyFixture() - { - _source = new SourceList(); - _results = _source.Connect().GroupOnPropertyWithImmutableState(p=>p.Age).AsAggregator(); - } + private readonly ISourceList _source; - public void Dispose() + public GroupOnPropertyFixture() { - _source.Dispose(); - _results.Dispose(); + _source = new SourceList(); + _results = _source.Connect().GroupOnPropertyWithImmutableState(p => p.Age).AsAggregator(); } [Fact] public void CanGroupOnAdds() { - _source.Add(new Person("A",10)); + _source.Add(new Person("A", 10)); _results.Data.Count.Should().Be(1); - var firstGroup= _results.Data.Items.First(); - - firstGroup.Count.Should().Be(1); - firstGroup.Key.Should().Be(10); - } - - [Fact] - public void CanRemoveFromGroup() - { - var person = new Person("A", 10); - _source.Add(person); - _source.Remove(person); - - _results.Data.Count.Should().Be(0); - } - - [Fact] - public void Regroup() - { - var person = new Person("A", 10); - _source.Add(person); - person.Age = 20; - - _results.Data.Count.Should().Be(1); var firstGroup = _results.Data.Items.First(); firstGroup.Count.Should().Be(1); - firstGroup.Key.Should().Be(20); + firstGroup.Key.Should().Be(10); } [Fact] @@ -85,19 +58,45 @@ public void CanHandleChangedItemsBatch() var initialCount = people.Select(p => p.Age).Distinct().Count(); _results.Data.Count.Should().Be(initialCount); - people.Take(25) - .ForEach(p=>p.Age=200); + people.Take(25).ForEach(p => p.Age = 200); - var changedCount = people.Select(p => p.Age).Distinct().Count(); + var changedCount = people.Select(p => p.Age).Distinct().Count(); _results.Data.Count.Should().Be(changedCount); //check that each item is only in one cache - var peopleInCache = _results.Data.Items - .SelectMany(g => g.Items) - .ToArray(); + var peopleInCache = _results.Data.Items.SelectMany(g => g.Items).ToArray(); peopleInCache.Length.Should().Be(100); + } + [Fact] + public void CanRemoveFromGroup() + { + var person = new Person("A", 10); + _source.Add(person); + _source.Remove(person); + + _results.Data.Count.Should().Be(0); + } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void Regroup() + { + var person = new Person("A", 10); + _source.Add(person); + person.Age = 20; + + _results.Data.Count.Should().Be(1); + var firstGroup = _results.Data.Items.First(); + + firstGroup.Count.Should().Be(1); + firstGroup.Key.Should().Be(20); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/GroupOnPropertyWithImmutableStateFixture.cs b/src/DynamicData.Tests/List/GroupOnPropertyWithImmutableStateFixture.cs index 343d5d6c7..e74fc6fa3 100644 --- a/src/DynamicData.Tests/List/GroupOnPropertyWithImmutableStateFixture.cs +++ b/src/DynamicData.Tests/List/GroupOnPropertyWithImmutableStateFixture.cs @@ -1,30 +1,27 @@ using System; using System.Linq; + using DynamicData.Kernel; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class GroupOnPropertyWithImmutableStateFixture: IDisposable + public class GroupOnPropertyWithImmutableStateFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator> _results; - public GroupOnPropertyWithImmutableStateFixture() + private readonly ISourceList _source; + + public GroupOnPropertyWithImmutableStateFixture() { _source = new SourceList(); _results = _source.Connect().GroupOnPropertyWithImmutableState(p => p.Age).AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void CanGroupOnAdds() { @@ -38,30 +35,6 @@ public void CanGroupOnAdds() firstGroup.Key.Should().Be(10); } - [Fact] - public void CanRemoveFromGroup() - { - var person = new Person("A", 10); - _source.Add(person); - _source.Remove(person); - - _results.Data.Count.Should().Be(0); - } - - [Fact] - public void Regroup() - { - var person = new Person("A", 10); - _source.Add(person); - person.Age = 20; - - _results.Data.Count.Should().Be(1); - var firstGroup = _results.Data.Items.First(); - - firstGroup.Count.Should().Be(1); - firstGroup.Key.Should().Be(20); - } - [Fact] public void CanHandleAddBatch() { @@ -85,19 +58,45 @@ public void CanHandleChangedItemsBatch() var initialCount = people.Select(p => p.Age).Distinct().Count(); _results.Data.Count.Should().Be(initialCount); - people.Take(25) - .ForEach(p => p.Age = 200); + people.Take(25).ForEach(p => p.Age = 200); var changedCount = people.Select(p => p.Age).Distinct().Count(); _results.Data.Count.Should().Be(changedCount); //check that each item is only in one cache - var peopleInCache = _results.Data.Items - .SelectMany(g => g.Items) - .ToArray(); + var peopleInCache = _results.Data.Items.SelectMany(g => g.Items).ToArray(); peopleInCache.Length.Should().Be(100); + } + [Fact] + public void CanRemoveFromGroup() + { + var person = new Person("A", 10); + _source.Add(person); + _source.Remove(person); + + _results.Data.Count.Should().Be(0); + } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + + [Fact] + public void Regroup() + { + var person = new Person("A", 10); + _source.Add(person); + person.Age = 20; + + _results.Data.Count.Should().Be(1); + var firstGroup = _results.Data.Items.First(); + + firstGroup.Count.Should().Be(1); + firstGroup.Key.Should().Be(20); } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/List/MergeManyChangeSetsFixture.cs b/src/DynamicData.Tests/List/MergeManyChangeSetsFixture.cs index b2942dd64..107bcb532 100644 --- a/src/DynamicData.Tests/List/MergeManyChangeSetsFixture.cs +++ b/src/DynamicData.Tests/List/MergeManyChangeSetsFixture.cs @@ -1,4 +1,5 @@ using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List @@ -17,9 +18,7 @@ public void MergeManyShouldWork() parent.Add(b); parent.Add(c); - var d = parent.Connect() - .MergeMany(e => e.Connect().RemoveIndex()) - .AsObservableList(); + var d = parent.Connect().MergeMany(e => e.Connect().RemoveIndex()).AsObservableList(); 0.Should().Be(d.Count); @@ -33,13 +32,13 @@ public void MergeManyShouldWork() 3.Should().Be(d.Count); b.Add(5); 4.Should().Be(d.Count); - new[] {1, 2, 3, 5}.Should().BeEquivalentTo(d.Items); + new[] { 1, 2, 3, 5 }.Should().BeEquivalentTo(d.Items); b.Clear(); // Fails below 2.Should().Be(d.Count); - new[] {1, 2}.Should().BeEquivalentTo(d.Items); + new[] { 1, 2 }.Should().BeEquivalentTo(d.Items); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/MergeManyFixture.cs b/src/DynamicData.Tests/List/MergeManyFixture.cs index 2ac3da534..3a54405d1 100644 --- a/src/DynamicData.Tests/List/MergeManyFixture.cs +++ b/src/DynamicData.Tests/List/MergeManyFixture.cs @@ -1,38 +1,17 @@ using System; using System.Reactive.Subjects; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class MergeManyFixture: IDisposable + public class MergeManyFixture : IDisposable { - private class ObjectWithObservable - { - private readonly int _id; - private readonly ISubject _changed = new Subject(); - private bool _value; - - public ObjectWithObservable(int id) - { - _id = id; - } - - public void InvokeObservable(bool value) - { - _value = value; - _changed.OnNext(value); - } - - public IObservable Observable => _changed; - - public int Id => _id; - } - private readonly ISourceList _source; - public MergeManyFixture() + public MergeManyFixture() { _source = new SourceList(); } @@ -42,6 +21,21 @@ public void Dispose() _source.Dispose(); } + [Fact] + public void EverythingIsUnsubscribedWhenStreamIsDisposed() + { + bool invoked = false; + var stream = _source.Connect().MergeMany(o => o.Observable).Subscribe(o => { invoked = true; }); + + var item = new ObjectWithObservable(1); + _source.Add(item); + + stream.Dispose(); + + item.InvokeObservable(true); + invoked.Should().BeFalse(); + } + /// /// Invocations the only when child is invoked. /// @@ -50,9 +44,7 @@ public void InvocationOnlyWhenChildIsInvoked() { bool invoked = false; - var stream = _source.Connect() - .MergeMany(o => o.Observable) - .Subscribe(o => { invoked = true; }); + var stream = _source.Connect().MergeMany(o => o.Observable).Subscribe(o => { invoked = true; }); var item = new ObjectWithObservable(1); _source.Add(item); @@ -68,9 +60,7 @@ public void InvocationOnlyWhenChildIsInvoked() public void RemovedItemWillNotCauseInvocation() { bool invoked = false; - var stream = _source.Connect() - .MergeMany(o => o.Observable) - .Subscribe(o => { invoked = true; }); + var stream = _source.Connect().MergeMany(o => o.Observable).Subscribe(o => { invoked = true; }); var item = new ObjectWithObservable(1); _source.Add(item); @@ -82,21 +72,26 @@ public void RemovedItemWillNotCauseInvocation() stream.Dispose(); } - [Fact] - public void EverythingIsUnsubscribedWhenStreamIsDisposed() + private class ObjectWithObservable { - bool invoked = false; - var stream = _source.Connect() - .MergeMany(o => o.Observable) - .Subscribe(o => { invoked = true; }); + private readonly ISubject _changed = new Subject(); - var item = new ObjectWithObservable(1); - _source.Add(item); + private bool _value; - stream.Dispose(); + public ObjectWithObservable(int id) + { + Id = id; + } - item.InvokeObservable(true); - invoked.Should().BeFalse(); + public int Id { get; } + + public IObservable Observable => _changed; + + public void InvokeObservable(bool value) + { + _value = value; + _changed.OnNext(value); + } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/OrFixture.cs b/src/DynamicData.Tests/List/OrFixture.cs index 85a7b4f33..0c23ffaa1 100644 --- a/src/DynamicData.Tests/List/OrFixture.cs +++ b/src/DynamicData.Tests/List/OrFixture.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class OrFixture : OrFixtureBase { protected override IObservable> CreateObservable() @@ -29,9 +30,9 @@ public class OrRefreshFixture [Fact] public void RefreshPassesThrough() { - SourceList source1 = new SourceList(); + SourceList source1 = new(); source1.Add(new Item("A")); - SourceList source2 = new SourceList(); + SourceList source2 = new(); source2.Add(new Item("B")); var list = new List>> { source1.Connect().AutoRefresh(), source2.Connect().AutoRefresh() }; @@ -54,9 +55,9 @@ public void ItemIsReplaced() var item2 = new Item("B"); var item1Replacement = new Item("Test"); - SourceList source1 = new SourceList(); + SourceList source1 = new(); source1.Add(item1); - SourceList source2 = new SourceList(); + SourceList source2 = new(); source2.Add(item2); var list = new List>> { source1.Connect(), source2.Connect() }; @@ -69,10 +70,12 @@ public void ItemIsReplaced() } } - public abstract class OrFixtureBase: IDisposable + public abstract class OrFixtureBase : IDisposable { protected ISourceList _source1; + protected ISourceList _source2; + private readonly ChangeSetAggregator _results; protected OrFixtureBase() @@ -82,7 +85,24 @@ protected OrFixtureBase() _results = CreateObservable().AsAggregator(); } - protected abstract IObservable> CreateObservable(); + [Fact] + public void ClearOnlyClearsOneSource() + { + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _source1.Clear(); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); + } + + [Fact] + public void CombineRange() + { + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _results.Data.Count.Should().Be(10); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); + } public void Dispose() { @@ -117,23 +137,6 @@ public void RemovedWhenNoLongerInEither() _results.Data.Count.Should().Be(0); } - [Fact] - public void CombineRange() - { - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _results.Data.Count.Should().Be(10); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); - } - - [Fact] - public void ClearOnlyClearsOneSource() - { - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _source1.Clear(); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); - } + protected abstract IObservable> CreateObservable(); } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/PageFixture.cs b/src/DynamicData.Tests/List/PageFixture.cs index dd4fadf67..755349611 100644 --- a/src/DynamicData.Tests/List/PageFixture.cs +++ b/src/DynamicData.Tests/List/PageFixture.cs @@ -2,22 +2,27 @@ using System.Linq; using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Binding; using DynamicData.Tests.Domain; -using FluentAssertions; + +using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class PageFixture: IDisposable + public class PageFixture : IDisposable { - private readonly ISourceList _source; - private readonly ChangeSetAggregator _results; + private readonly RandomPersonGenerator _generator = new(); + private readonly ISubject _requestSubject = new BehaviorSubject(new PageRequest(1, 25)); - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); - public PageFixture() + private readonly ChangeSetAggregator _results; + + private readonly ISourceList _source; + + public PageFixture() { _source = new SourceList(); _results = _source.Connect().Page(_requestSubject).AsAggregator(); @@ -30,26 +35,6 @@ public void Dispose() _results.Dispose(); } - [Fact] - public void VirtualiseInitial() - { - var people = _generator.Take(100).ToArray(); - _source.AddRange(people); - var expected = people.Take(25).ToArray(); - _results.Data.Items.Should().BeEquivalentTo(expected); - } - - [Fact] - public void MoveToNextPage() - { - var people = _generator.Take(100).ToArray(); - _source.AddRange(people); - _requestSubject.OnNext(new PageRequest(2, 25)); - - var expected = people.Skip(25).Take(25).ToArray(); - _results.Data.Items.Should().BeEquivalentTo(expected); - } - [Fact] public void InsertAfterPageProducesNothing() { @@ -79,25 +64,14 @@ public void InsertInPageReflectsChange() } [Fact] - public void RemoveBeforeShiftsPage() + public void MoveToNextPage() { var people = _generator.Take(100).ToArray(); _source.AddRange(people); _requestSubject.OnNext(new PageRequest(2, 25)); - _source.RemoveAt(0); - var expected = people.Skip(26).Take(25).ToArray(); + var expected = people.Skip(25).Take(25).ToArray(); _results.Data.Items.Should().BeEquivalentTo(expected); - - var removedMessage = _results.Messages[2].ElementAt(0); - var removedPerson = people.ElementAt(25); - removedMessage.Item.Current.Should().Be(removedPerson); - removedMessage.Reason.Should().Be(ListChangeReason.Remove); - - var addedMessage = _results.Messages[2].ElementAt(1); - var addedPerson = people.ElementAt(50); - addedMessage.Item.Current.Should().Be(addedPerson); - addedMessage.Reason.Should().Be(ListChangeReason.Add); } [Fact] @@ -123,68 +97,96 @@ public void MoveWithinSamePage2() var actualPersonAtIndex0 = _results.Data.Items.ElementAt(0); actualPersonAtIndex0.Should().Be(personToMove); } + + [Fact] + public void RemoveBeforeShiftsPage() + { + var people = _generator.Take(100).ToArray(); + _source.AddRange(people); + _requestSubject.OnNext(new PageRequest(2, 25)); + _source.RemoveAt(0); + var expected = people.Skip(26).Take(25).ToArray(); + + _results.Data.Items.Should().BeEquivalentTo(expected); + + var removedMessage = _results.Messages[2].ElementAt(0); + var removedPerson = people.ElementAt(25); + removedMessage.Item.Current.Should().Be(removedPerson); + removedMessage.Reason.Should().Be(ListChangeReason.Remove); + + var addedMessage = _results.Messages[2].ElementAt(1); + var addedPerson = people.ElementAt(50); + addedMessage.Item.Current.Should().Be(addedPerson); + addedMessage.Reason.Should().Be(ListChangeReason.Add); + } + + [Fact] + public void VirtualiseInitial() + { + var people = _generator.Take(100).ToArray(); + _source.AddRange(people); + var expected = people.Take(25).ToArray(); + _results.Data.Items.Should().BeEquivalentTo(expected); + } } public class PageFixtureWithNoInitialData { private readonly Animal[] _items = - { - new Animal("Holly", "Cat", AnimalFamily.Mammal), - new Animal("Rover", "Dog", AnimalFamily.Mammal), - new Animal("Rex", "Dog", AnimalFamily.Mammal), - new Animal("Whiskers", "Cat", AnimalFamily.Mammal), - new Animal("Nemo", "Fish", AnimalFamily.Fish), - new Animal("Moby Dick", "Whale", AnimalFamily.Mammal), - new Animal("Fred", "Frog", AnimalFamily.Amphibian), - new Animal("Isaac", "Next", AnimalFamily.Amphibian), - new Animal("Sam", "Snake", AnimalFamily.Reptile), - new Animal("Sharon", "Red Backed Shrike", AnimalFamily.Bird), - }; + { + new("Holly", "Cat", AnimalFamily.Mammal), + new("Rover", "Dog", AnimalFamily.Mammal), + new("Rex", "Dog", AnimalFamily.Mammal), + new("Whiskers", "Cat", AnimalFamily.Mammal), + new("Nemo", "Fish", AnimalFamily.Fish), + new("Moby Dick", "Whale", AnimalFamily.Mammal), + new("Fred", "Frog", AnimalFamily.Amphibian), + new("Isaac", "Next", AnimalFamily.Amphibian), + new("Sam", "Snake", AnimalFamily.Reptile), + new("Sharon", "Red Backed Shrike", AnimalFamily.Bird), + }; [Fact] - public void SimplePagging() + public void SimplePaging() { - using (var pager = new BehaviorSubject(new PageRequest(0, 0))) - using (var sourceList = new SourceList()) - using (var sut = new SimplePagging(sourceList, pager)) - { - // Add items to source - sourceList.AddRange(_items); + using var pager = new BehaviorSubject(new PageRequest(0, 0)); + using var sourceList = new SourceList(); + using var sut = new SimplePaging(sourceList, pager); + // Add items to source + sourceList.AddRange(_items); - sut.Paged.Count.Should().Be(0); + sut.Paged.Count.Should().Be(0); - pager.OnNext(new PageRequest(1, 2)); - sut.Paged.Count.Should().Be(2); + pager.OnNext(new PageRequest(1, 2)); + sut.Paged.Count.Should().Be(2); - pager.OnNext(new PageRequest(1, 4)); - sut.Paged.Count.Should().Be(4); + pager.OnNext(new PageRequest(1, 4)); + sut.Paged.Count.Should().Be(4); - pager.OnNext(new PageRequest(2, 3)); - sut.Paged.Count.Should().Be(3); - } + pager.OnNext(new PageRequest(2, 3)); + sut.Paged.Count.Should().Be(3); } } - public class SimplePagging : AbstractNotifyPropertyChanged, IDisposable + public class SimplePaging : AbstractNotifyPropertyChanged, IDisposable { private readonly IDisposable _cleanUp; - public IObservableList Paged { get; } - - public SimplePagging(IObservableList source, IObservable pager) + public SimplePaging(IObservableList source, IObservable pager) { Paged = source.Connect() .Page(pager) - .Do(changes=>Console.WriteLine(changes.TotalChanges)) //added as a quick and dirty way to debug + .Do(changes => Console.WriteLine(changes.TotalChanges)) //added as a quick and dirty way to debug .AsObservableList(); _cleanUp = Paged; } + public IObservableList Paged { get; } + public void Dispose() { _cleanUp.Dispose(); } } - -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/QueryWhenChangedFixture.cs b/src/DynamicData.Tests/List/QueryWhenChangedFixture.cs index 1af18efc7..5764a0782 100644 --- a/src/DynamicData.Tests/List/QueryWhenChangedFixture.cs +++ b/src/DynamicData.Tests/List/QueryWhenChangedFixture.cs @@ -1,64 +1,60 @@ -using DynamicData.Tests.Domain; -using Xunit; using System; + +using DynamicData.Tests.Domain; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class QueryWhenChangedFixture: IDisposable + public class QueryWhenChangedFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public QueryWhenChangedFixture() + private readonly ISourceList _source; + + public QueryWhenChangedFixture() { _source = new SourceList(); _results = new ChangeSetAggregator(_source.Connect(p => p.Age > 20)); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] - public void ChangeInvokedOnSubscriptionIfItHasData() + public void CanHandleAddsAndUpdates() { bool invoked = false; - _source.Add(new Person("A", 1)); - var subscription = _source.Connect() - .QueryWhenChanged() - .Subscribe(x => invoked = true); + var subscription = _source.Connect().QueryWhenChanged(q => q.Count).Subscribe(query => invoked = true); + + var person = new Person("A", 1); + _source.Add(person); + _source.Remove(person); + invoked.Should().BeTrue(); subscription.Dispose(); } [Fact] - public void CanHandleAddsAndUpdates() + public void ChangeInvokedOnNext() { bool invoked = false; - var subscription = _source.Connect() - .QueryWhenChanged(q => q.Count) - .Subscribe(query => invoked = true); - var person = new Person("A", 1); - _source.Add(person); - _source.Remove(person); + var subscription = _source.Connect().QueryWhenChanged().Subscribe(x => invoked = true); + invoked.Should().BeFalse(); + + _source.Add(new Person("A", 1)); invoked.Should().BeTrue(); + subscription.Dispose(); } [Fact] - public void ChangeInvokedOnNext() + public void ChangeInvokedOnNext_WithSelector() { bool invoked = false; - var subscription = _source.Connect() - .QueryWhenChanged() - .Subscribe(x => invoked = true); + var subscription = _source.Connect().QueryWhenChanged(query => query.Count).Subscribe(x => invoked = true); invoked.Should().BeFalse(); @@ -69,32 +65,29 @@ public void ChangeInvokedOnNext() } [Fact] - public void ChangeInvokedOnSubscriptionIfItHasData_WithSelector() + public void ChangeInvokedOnSubscriptionIfItHasData() { bool invoked = false; _source.Add(new Person("A", 1)); - var subscription = _source.Connect() - .QueryWhenChanged(query => query.Count) - .Subscribe(x => invoked = true); + var subscription = _source.Connect().QueryWhenChanged().Subscribe(x => invoked = true); invoked.Should().BeTrue(); subscription.Dispose(); } [Fact] - public void ChangeInvokedOnNext_WithSelector() + public void ChangeInvokedOnSubscriptionIfItHasData_WithSelector() { bool invoked = false; - - var subscription = _source.Connect() - .QueryWhenChanged(query => query.Count) - .Subscribe(x => invoked = true); - - invoked.Should().BeFalse(); - _source.Add(new Person("A", 1)); + var subscription = _source.Connect().QueryWhenChanged(query => query.Count).Subscribe(x => invoked = true); invoked.Should().BeTrue(); - subscription.Dispose(); } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/RecursiveTransformManyFixture.cs b/src/DynamicData.Tests/List/RecursiveTransformManyFixture.cs index 20fadfe41..25a6260a9 100644 --- a/src/DynamicData.Tests/List/RecursiveTransformManyFixture.cs +++ b/src/DynamicData.Tests/List/RecursiveTransformManyFixture.cs @@ -1,23 +1,40 @@ using System; + using DynamicData.Tests.Domain; using DynamicData.Tests.Utilities; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class RecursiveTransformManyFixture: IDisposable + public class RecursiveTransformManyFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public RecursiveTransformManyFixture() + private readonly ISourceList _source; + + public RecursiveTransformManyFixture() { _source = new SourceList(); - _results = _source.Connect().TransformMany(p => p.Relations.RecursiveSelect(r => r.Relations)) - .AsAggregator(); + _results = _source.Connect().TransformMany(p => p.Relations.RecursiveSelect(r => r.Relations)).AsAggregator(); + } + + [Fact] + public void ChildrenAreRemovedWhenParentIsRemoved() + { + var frientofchild1 = new PersonWithRelations("Friend1", 10); + var child1 = new PersonWithRelations("Child1", 10, new[] { frientofchild1 }); + var child2 = new PersonWithRelations("Child2", 8); + var child3 = new PersonWithRelations("Child3", 8); + var mother = new PersonWithRelations("Mother", 35, new[] { child1, child2, child3 }); + // var father = new PersonWithRelations("Father", 35, new[] {child1, child2, child3, mother}); + + _source.Add(mother); + _source.Remove(mother); + _results.Data.Count.Should().Be(0, "Should be 4 in the cache"); } public void Dispose() @@ -43,20 +60,5 @@ public void RecursiveChildrenCanBeAdded() _results.Data.Items.IndexOfOptional(child3).HasValue.Should().BeTrue(); _results.Data.Items.IndexOfOptional(frientofchild1).HasValue.Should().BeTrue(); } - - [Fact] - public void ChildrenAreRemovedWhenParentIsRemoved() - { - var frientofchild1 = new PersonWithRelations("Friend1", 10); - var child1 = new PersonWithRelations("Child1", 10, new[] {frientofchild1}); - var child2 = new PersonWithRelations("Child2", 8); - var child3 = new PersonWithRelations("Child3", 8); - var mother = new PersonWithRelations("Mother", 35, new[] {child1, child2, child3}); - // var father = new PersonWithRelations("Father", 35, new[] {child1, child2, child3, mother}); - - _source.Add(mother); - _source.Remove(mother); - _results.Data.Count.Should().Be(0, "Should be 4 in the cache"); - } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/List/RefCountFixture.cs b/src/DynamicData.Tests/List/RefCountFixture.cs index 104b12008..9f911eeb2 100644 --- a/src/DynamicData.Tests/List/RefCountFixture.cs +++ b/src/DynamicData.Tests/List/RefCountFixture.cs @@ -1,26 +1,45 @@ using System; +using System.Linq; using System.Reactive.Linq; -using DynamicData.Tests.Domain; -using Xunit; using System.Threading.Tasks; -using System.Linq; + +using DynamicData.Tests.Domain; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class RefCountFixture: IDisposable + public class RefCountFixture : IDisposable { private readonly ISourceList _source; - public RefCountFixture() + public RefCountFixture() { _source = new SourceList(); } - public void Dispose() + [Fact] + public void CanResubscribe() { - _source.Dispose(); + int created = 0; + int disposals = 0; + + //must have data so transform is invoked + _source.Add(new Person("Name", 10)); + + //Some expensive transform (or chain of operations) + var longChain = _source.Connect().Transform(p => p).Do(_ => created++).Finally(() => disposals++).RefCount(); + + var subscriber = longChain.Subscribe(); + subscriber.Dispose(); + + subscriber = longChain.Subscribe(); + subscriber.Dispose(); + + created.Should().Be(2); + disposals.Should().Be(2); } [Fact] @@ -30,11 +49,7 @@ public void ChainIsInvokedOnceForMultipleSubscribers() int disposals = 0; //Some expensive transform (or chain of operations) - var longChain = _source.Connect() - .Transform(p => p) - .Do(_ => created++) - .Finally(() => disposals++) - .RefCount(); + var longChain = _source.Connect().Transform(p => p).Do(_ => created++).Finally(() => disposals++).RefCount(); var suscriber1 = longChain.Subscribe(); var suscriber2 = longChain.Subscribe(); @@ -49,30 +64,9 @@ public void ChainIsInvokedOnceForMultipleSubscribers() disposals.Should().Be(1); } - [Fact] - public void CanResubscribe() + public void Dispose() { - int created = 0; - int disposals = 0; - - //must have data so transform is invoked - _source.Add(new Person("Name", 10)); - - //Some expensive transform (or chain of operations) - var longChain = _source.Connect() - .Transform(p => p) - .Do(_ => created++) - .Finally(() => disposals++) - .RefCount(); - - var subscriber = longChain.Subscribe(); - subscriber.Dispose(); - - subscriber = longChain.Subscribe(); - subscriber.Dispose(); - - created.Should().Be(2); - disposals.Should().Be(2); + _source.Dispose(); } // This test is probabilistic, it could be cool to be able to prove RefCount's thread-safety @@ -83,15 +77,17 @@ private async Task IsHopefullyThreadSafe() { var refCount = _source.Connect().RefCount(); - await Task.WhenAll(Enumerable.Range(0, 100).Select(_ => - Task.Run(() => - { - for (int i = 0; i < 1000; ++i) - { - var subscription = refCount.Subscribe(); - subscription.Dispose(); - } - }))); + await Task.WhenAll( + Enumerable.Range(0, 100).Select( + _ => Task.Run( + () => + { + for (int i = 0; i < 1000; ++i) + { + var subscription = refCount.Subscribe(); + subscription.Dispose(); + } + }))); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/RemoveManyFixture.cs b/src/DynamicData.Tests/List/RemoveManyFixture.cs index caf3a2b53..e429d7017 100644 --- a/src/DynamicData.Tests/List/RemoveManyFixture.cs +++ b/src/DynamicData.Tests/List/RemoveManyFixture.cs @@ -1,35 +1,28 @@ using System; using System.Collections.Generic; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class RemoveManyFixture { private readonly List _list; - public RemoveManyFixture() + public RemoveManyFixture() { _list = new List(); } - [Fact] - public void RemoveManyWillRemoveARange() - { - _list.AddRange(Enumerable.Range(1, 10)); - _list.RemoveMany(Enumerable.Range(2, 8)); - _list.Should().BeEquivalentTo(new[] {1, 10}); - } - [Fact] public void DoesNotRemoveDuplicates() { - _list.AddRange(new[] {1, 1, 1, 5, 6, 7}); - _list.RemoveMany(new[] {1, 1, 7}); - _list.Should().BeEquivalentTo(new[] {1, 5, 6}); + _list.AddRange(new[] { 1, 1, 1, 5, 6, 7 }); + _list.RemoveMany(new[] { 1, 1, 7 }); + _list.Should().BeEquivalentTo(new[] { 1, 5, 6 }); } [Fact] @@ -42,5 +35,13 @@ public void RemoveLargeBatch() _list.RemoveMany(toRemove); _list.Should().BeEquivalentTo(toAdd.Except(toRemove)); } + + [Fact] + public void RemoveManyWillRemoveARange() + { + _list.AddRange(Enumerable.Range(1, 10)); + _list.RemoveMany(Enumerable.Range(2, 8)); + _list.Should().BeEquivalentTo(new[] { 1, 10 }); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/ReverseFixture.cs b/src/DynamicData.Tests/List/ReverseFixture.cs index 872cbabae..6c501885d 100644 --- a/src/DynamicData.Tests/List/ReverseFixture.cs +++ b/src/DynamicData.Tests/List/ReverseFixture.cs @@ -1,28 +1,24 @@ using System; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class ReverseFixture: IDisposable + public class ReverseFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public ReverseFixture() + private readonly ISourceList _source; + + public ReverseFixture() { _source = new SourceList(); _results = _source.Connect().Reverse().AsAggregator(); } - public void Dispose() - { - _results.Dispose(); - _source.Dispose(); - } - [Fact] public void AddInSucession() { @@ -32,72 +28,78 @@ public void AddInSucession() _source.Add(4); _source.Add(5); - _results.Data.Items.Should().BeEquivalentTo(new[] {5, 4, 3, 2, 1}); + _results.Data.Items.Should().BeEquivalentTo(new[] { 5, 4, 3, 2, 1 }); } [Fact] public void AddRange() { _source.AddRange(Enumerable.Range(1, 5)); - _results.Data.Items.Should().BeEquivalentTo(new[] {5, 4, 3, 2, 1}); + _results.Data.Items.Should().BeEquivalentTo(new[] { 5, 4, 3, 2, 1 }); } [Fact] - public void Removes() + public void Clear() { _source.AddRange(Enumerable.Range(1, 5)); - _source.Remove(1); - _source.Remove(4); - _results.Data.Items.Should().BeEquivalentTo(new[] {5, 3, 2}); + _source.Clear(); + _results.Data.Count.Should().Be(0); + } + + public void Dispose() + { + _results.Dispose(); + _source.Dispose(); } [Fact] - public void RemoveRange() + public void Move() { _source.AddRange(Enumerable.Range(1, 5)); - _source.RemoveRange(1, 3); - _results.Data.Items.Should().BeEquivalentTo(new[] {5, 1}); + _source.Move(4, 1); + _results.Data.Items.Should().BeEquivalentTo(new[] { 4, 3, 2, 5, 1 }); } [Fact] - public void RemoveRangeThenInsert() + public void Move2() { _source.AddRange(Enumerable.Range(1, 5)); - _source.RemoveRange(1, 3); - _source.Insert(1, 3); - _results.Data.Items.Should().BeEquivalentTo(new[] {5, 3, 1}); + _source.Move(1, 4); + _results.Data.Items.Should().BeEquivalentTo(new[] { 2, 5, 4, 3, 1 }); } [Fact] - public void Replace() + public void RemoveRange() { _source.AddRange(Enumerable.Range(1, 5)); - _source.ReplaceAt(2, 100); - _results.Data.Items.Should().BeEquivalentTo(new[] {5, 4, 100, 2, 1}); + _source.RemoveRange(1, 3); + _results.Data.Items.Should().BeEquivalentTo(new[] { 5, 1 }); } [Fact] - public void Clear() + public void RemoveRangeThenInsert() { _source.AddRange(Enumerable.Range(1, 5)); - _source.Clear(); - _results.Data.Count.Should().Be(0); + _source.RemoveRange(1, 3); + _source.Insert(1, 3); + _results.Data.Items.Should().BeEquivalentTo(new[] { 5, 3, 1 }); } [Fact] - public void Move() + public void Removes() { _source.AddRange(Enumerable.Range(1, 5)); - _source.Move(4, 1); - _results.Data.Items.Should().BeEquivalentTo(new[] {4, 3, 2, 5, 1}); + _source.Remove(1); + _source.Remove(4); + _results.Data.Items.Should().BeEquivalentTo(new[] { 5, 3, 2 }); } [Fact] - public void Move2() + public void Replace() { _source.AddRange(Enumerable.Range(1, 5)); - _source.Move(1, 4); - _results.Data.Items.Should().BeEquivalentTo(new[] {2, 5, 4, 3, 1}); + _source.ReplaceAt(2, 100); + _results.Data.Items.Should().BeEquivalentTo(new[] { 5, 4, 100, 2, 1 }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/SelectFixture.cs b/src/DynamicData.Tests/List/SelectFixture.cs index 587d709ca..f5962613b 100644 --- a/src/DynamicData.Tests/List/SelectFixture.cs +++ b/src/DynamicData.Tests/List/SelectFixture.cs @@ -1,36 +1,33 @@ using System; using System.Linq; + using DynamicData.Alias; using DynamicData.Tests.Domain; -using Xunit; + using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class SelectFixture: IDisposable + public class SelectFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; + private readonly ISourceList _source; + private readonly Func _transformFactory = p => - { - string gender = p.Age % 2 == 0 ? "M" : "F"; - return new PersonWithGender(p, gender); - }; + { + string gender = p.Age % 2 == 0 ? "M" : "F"; + return new PersonWithGender(p, gender); + }; - public SelectFixture() + public SelectFixture() { _source = new SourceList(); _results = new ChangeSetAggregator(_source.Connect().Select(_transformFactory)); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void Add() { @@ -43,48 +40,53 @@ public void Add() } [Fact] - public void Remove() + public void BatchOfUniqueUpdates() { - const string key = "Adult1"; - var person = new Person(key, 50); + var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - _source.Add(person); - _source.Remove(person); + _source.AddRange(people); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 80 addes"); - _results.Messages[1].Removes.Should().Be(1, "Should be 80 removes"); - _results.Data.Count.Should().Be(0, "Should be nothing cached"); + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); + + var transformed = people.Select(_transformFactory).OrderBy(p => p.Age).ToArray(); + _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(transformed, "Incorrect transform result"); } [Fact] - public void Update() + public void Clear() { - const string key = "Adult1"; - var newperson = new Person(key, 50); - var updated = new Person(key, 51); + var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - _source.Add(newperson); - _source.Add(updated); + _source.AddRange(people); + _source.Clear(); _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - _results.Messages[0].Replaced.Should().Be(0, "Should be 1 update"); + _results.Messages[0].Adds.Should().Be(100, "Should be 80 addes"); + _results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); + _results.Data.Count.Should().Be(0, "Should be nothing cached"); } - [Fact] - public void BatchOfUniqueUpdates() + public void Dispose() { - var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); + _source.Dispose(); + _results.Dispose(); + } - _source.AddRange(people); + [Fact] + public void Remove() + { + const string key = "Adult1"; + var person = new Person(key, 50); - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); + _source.Add(person); + _source.Remove(person); - var transformed = people.Select(_transformFactory).OrderBy(p => p.Age).ToArray(); - _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(transformed, "Incorrect transform result"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(1, "Should be 80 addes"); + _results.Messages[1].Removes.Should().Be(1, "Should be 80 removes"); + _results.Data.Count.Should().Be(0, "Should be nothing cached"); } [Fact] @@ -100,17 +102,18 @@ public void SameKeyChanges() } [Fact] - public void Clear() + public void Update() { - var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); + const string key = "Adult1"; + var newperson = new Person(key, 50); + var updated = new Person(key, 51); - _source.AddRange(people); - _source.Clear(); + _source.Add(newperson); + _source.Add(updated); _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should be 80 addes"); - _results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); - _results.Data.Count.Should().Be(0, "Should be nothing cached"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); + _results.Messages[0].Replaced.Should().Be(0, "Should be 1 update"); } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/List/SizeLimitFixture.cs b/src/DynamicData.Tests/List/SizeLimitFixture.cs index cfae9f31e..474914013 100644 --- a/src/DynamicData.Tests/List/SizeLimitFixture.cs +++ b/src/DynamicData.Tests/List/SizeLimitFixture.cs @@ -1,22 +1,29 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + +using FluentAssertions; + using Microsoft.Reactive.Testing; + using Xunit; -using FluentAssertions; namespace DynamicData.Tests.List { - - public class SizeLimitFixture: IDisposable + public class SizeLimitFixture : IDisposable { - private readonly ISourceList _source; + private readonly RandomPersonGenerator _generator = new(); + private readonly ChangeSetAggregator _results; + private readonly TestScheduler _scheduler; + private readonly IDisposable _sizeLimiter; - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); - public SizeLimitFixture() + private readonly ISourceList _source; + + public SizeLimitFixture() { _scheduler = new TestScheduler(); _source = new SourceList(); @@ -24,11 +31,15 @@ public SizeLimitFixture() _results = _source.Connect().AsAggregator(); } - public void Dispose() + [Fact] + public void Add() { - _sizeLimiter.Dispose(); - _source.Dispose(); - _results.Dispose(); + var person = _generator.Take(1).First(); + _source.Add(person); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + _results.Data.Items.First().Should().Be(person, "Should be same person"); } [Fact] @@ -72,19 +83,14 @@ public void AddMoreThanLimitInBatched() _results.Messages[2].Removes.Should().Be(10, "Should be 10 removes in the third update"); } - [Fact] - public void Add() + public void Dispose() { - var person = _generator.Take(1).First(); - _source.Add(person); - - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - _results.Data.Items.First().Should().Be(person, "Should be same person"); + _sizeLimiter.Dispose(); + _source.Dispose(); + _results.Dispose(); } [Fact] - public void ForceError() { var person = _generator.Take(1).First(); @@ -98,4 +104,4 @@ public void ThrowsIfSizeLimitIsZero() Assert.Throws(() => new SourceCache(p => p.Key).LimitSizeTo(0)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/SortFixture.cs b/src/DynamicData.Tests/List/SortFixture.cs index f7a479cd5..1d7311908 100644 --- a/src/DynamicData.Tests/List/SortFixture.cs +++ b/src/DynamicData.Tests/List/SortFixture.cs @@ -1,25 +1,27 @@ - -using System; +using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Binding; using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class SortFixture: IDisposable + public class SortFixture : IDisposable { - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); - private readonly ISourceList _source; + private readonly IComparer _comparer = SortExpressionComparer.Ascending(p => p.Name).ThenByAscending(p => p.Age); + + private readonly RandomPersonGenerator _generator = new(); + private readonly ChangeSetAggregator _results; - private readonly IComparer _comparer = SortExpressionComparer - .Ascending(p => p.Name) - .ThenByAscending(p => p.Age); + private readonly ISourceList _source; - public SortFixture() + public SortFixture() { _source = new SourceList(); _results = _source.Connect().Sort(_comparer).AsAggregator(); @@ -31,20 +33,6 @@ public void Dispose() _source.Dispose(); } - [Fact] - public void SortInitialBatch() - { - var people = _generator.Take(100).ToArray(); - _source.AddRange(people); - - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - - var expectedResult = people.OrderBy(p => p, _comparer); - var actualResult = _results.Data.Items; - - actualResult.Should().BeEquivalentTo(expectedResult); - } - [Fact] public void Insert() { @@ -60,34 +48,38 @@ public void Insert() } [Fact] - public void Replace() + public void Remove() { - var people = _generator.Take(100).ToArray(); + var people = _generator.Take(100).ToList(); _source.AddRange(people); - var shouldbefirst = new Person("__A", 99); - _source.ReplaceAt(10, shouldbefirst); + var toRemove = people.ElementAt(20); + people.RemoveAt(20); + _source.RemoveAt(20); - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + _results.Messages[1].First().Item.Current.Should().Be(toRemove, "Incorrect item removed"); - _results.Data.Items.First().Should().Be(shouldbefirst); + var expectedResult = people.OrderBy(p => p, _comparer); + var actualResult = _results.Data.Items; + actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void Remove() + public void RemoveManyOdds() { var people = _generator.Take(100).ToList(); _source.AddRange(people); - var toRemove = people.ElementAt(20); - people.RemoveAt(20); - _source.RemoveAt(20); + var odd = people.Select((p, idx) => new { p, idx }).Where(x => x.idx % 2 == 1).Select(x => x.p).ToArray(); - _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + _source.RemoveMany(odd); + + _results.Data.Count.Should().Be(50, "Should be 99 people in the cache"); _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - _results.Messages[1].First().Item.Current.Should().Be(toRemove, "Incorrect item removed"); - var expectedResult = people.OrderBy(p => p, _comparer); + var expectedResult = people.Except(odd).OrderByDescending(p => p, _comparer).ToArray(); var actualResult = _results.Data.Items; actualResult.Should().BeEquivalentTo(expectedResult); } @@ -125,21 +117,31 @@ public void RemoveManyReverseOrdered() } [Fact] - public void RemoveManyOdds() + public void Replace() { - var people = _generator.Take(100).ToList(); + var people = _generator.Take(100).ToArray(); _source.AddRange(people); - var odd = people.Select((p, idx) => new {p, idx}).Where(x => x.idx % 2 == 1).Select(x => x.p).ToArray(); + var shouldbefirst = new Person("__A", 99); + _source.ReplaceAt(10, shouldbefirst); + + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - _source.RemoveMany(odd); + _results.Data.Items.First().Should().Be(shouldbefirst); + } - _results.Data.Count.Should().Be(50, "Should be 99 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + [Fact] + public void SortInitialBatch() + { + var people = _generator.Take(100).ToArray(); + _source.AddRange(people); - var expectedResult = people.Except(odd).OrderByDescending(p => p, _comparer).ToArray(); + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + + var expectedResult = people.OrderBy(p => p, _comparer); var actualResult = _results.Data.Items; + actualResult.Should().BeEquivalentTo(expectedResult); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/SortMutableFixture.cs b/src/DynamicData.Tests/List/SortMutableFixture.cs index 97d8f3448..d1b6aa975 100644 --- a/src/DynamicData.Tests/List/SortMutableFixture.cs +++ b/src/DynamicData.Tests/List/SortMutableFixture.cs @@ -3,29 +3,32 @@ using System.Linq; using System.Reactive; using System.Reactive.Subjects; + using DynamicData.Binding; using DynamicData.Kernel; using DynamicData.Tests.Domain; -using Xunit; using FluentAssertions; +using Xunit; + namespace DynamicData.Tests.List { - - public class SortMutableFixture: IDisposable + public class SortMutableFixture : IDisposable { - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); - private readonly ISourceList _source; private readonly ISubject> _changeComparer; + + private readonly IComparer _comparer = SortExpressionComparer.Ascending(p => p.Age).ThenByAscending(p => p.Name); + + private readonly RandomPersonGenerator _generator = new(); + private readonly ISubject _resort; + private readonly ChangeSetAggregator _results; - private readonly IComparer _comparer = SortExpressionComparer - .Ascending(p => p.Age) - .ThenByAscending(p => p.Name); + private readonly ISourceList _source; - public SortMutableFixture() + public SortMutableFixture() { _source = new SourceList(); _changeComparer = new BehaviorSubject>(_comparer); @@ -34,26 +37,30 @@ public SortMutableFixture() _results = _source.Connect().Sort(_changeComparer, resetThreshold: 25, resort: _resort).AsAggregator(); } - public void Dispose() - { - _results.Dispose(); - _source.Dispose(); - } - [Fact] - public void SortInitialBatch() + public void ChangeComparer() { var people = _generator.Take(100).ToArray(); _source.AddRange(people); - _results.Data.Count.Should().Be(100); + var newComparer = SortExpressionComparer.Ascending(p => p.Name).ThenByAscending(p => p.Age); - var expectedResult = people.OrderBy(p => p, _comparer); + _changeComparer.OnNext(newComparer); + + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + + var expectedResult = people.OrderBy(p => p, newComparer); var actualResult = _results.Data.Items; actualResult.Should().BeEquivalentTo(expectedResult); } + public void Dispose() + { + _results.Dispose(); + _source.Dispose(); + } + [Fact] public void Insert() { @@ -69,34 +76,38 @@ public void Insert() } [Fact] - public void Replace() + public void Remove() { - var people = _generator.Take(100).ToArray(); + var people = _generator.Take(100).ToList(); _source.AddRange(people); - var shouldbeLast = new Person("__A", 999); - _source.ReplaceAt(10, shouldbeLast); + var toRemove = people.ElementAt(20); + people.RemoveAt(20); + _source.RemoveAt(20); - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + _results.Messages[1].First().Item.Current.Should().Be(toRemove, "Incorrect item removed"); - _results.Data.Items.Last().Should().Be(shouldbeLast); + var expectedResult = people.OrderBy(p => p, _comparer); + var actualResult = _results.Data.Items; + actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void Remove() + public void RemoveManyOdds() { var people = _generator.Take(100).ToList(); _source.AddRange(people); - var toRemove = people.ElementAt(20); - people.RemoveAt(20); - _source.RemoveAt(20); + var odd = people.Select((p, idx) => new { p, idx }).Where(x => x.idx % 2 == 1).Select(x => x.p).ToArray(); - _results.Data.Count.Should().Be(99, "Should be 99 people in the cache"); + _source.RemoveMany(odd); + + _results.Data.Count.Should().Be(50, "Should be 99 people in the cache"); _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); - _results.Messages[1].First().Item.Current.Should().Be(toRemove, "Incorrect item removed"); - var expectedResult = people.OrderBy(p => p, _comparer); + var expectedResult = people.Except(odd).OrderByDescending(p => p, _comparer).ToArray(); var actualResult = _results.Data.Items; actualResult.Should().BeEquivalentTo(expectedResult); } @@ -134,80 +145,70 @@ public void RemoveManyReverseOrdered() } [Fact] - public void ResortOnInlineChanges() + public void Replace() { - var people = _generator.Take(10).ToList(); + var people = _generator.Take(100).ToArray(); _source.AddRange(people); - people[0].Age = -1; - people[1].Age = -10; - people[2].Age = -12; - people[3].Age = -5; - people[4].Age = -7; - people[5].Age = -6; - - var comparer = SortExpressionComparer.Descending(p => p.Age) - .ThenByAscending(p => p.Name); - - _changeComparer.OnNext(comparer); + var shouldbeLast = new Person("__A", 999); + _source.ReplaceAt(10, shouldbeLast); - var expectedResult = people.OrderBy(p => p, comparer).ToArray(); - var actualResult = _results.Data.Items.ToArray(); + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - //actualResult.(expectedResult); - actualResult.Should().BeEquivalentTo(expectedResult); + _results.Data.Items.Last().Should().Be(shouldbeLast); } [Fact] - public void RemoveManyOdds() + public void Resort() { - var people = _generator.Take(100).ToList(); + var people = _generator.Take(100).ToArray(); _source.AddRange(people); - var odd = people.Select((p, idx) => new {p, idx}).Where(x => x.idx % 2 == 1).Select(x => x.p).ToArray(); + people.OrderBy(_ => Guid.NewGuid()).ForEach((person, index) => { person.Age = index; }); - _source.RemoveMany(odd); + _resort.OnNext(Unit.Default); - _results.Data.Count.Should().Be(50, "Should be 99 people in the cache"); - _results.Messages.Count.Should().Be(2, "Should be 2 update messages"); + _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); - var expectedResult = people.Except(odd).OrderByDescending(p => p, _comparer).ToArray(); + var expectedResult = people.OrderBy(p => p, _comparer); var actualResult = _results.Data.Items; + actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void Resort() + public void ResortOnInlineChanges() { - var people = _generator.Take(100).ToArray(); + var people = _generator.Take(10).ToList(); _source.AddRange(people); - people.OrderBy(_ => Guid.NewGuid()).ForEach((person, index) => { person.Age = index; }); + people[0].Age = -1; + people[1].Age = -10; + people[2].Age = -12; + people[3].Age = -5; + people[4].Age = -7; + people[5].Age = -6; - _resort.OnNext(Unit.Default); + var comparer = SortExpressionComparer.Descending(p => p.Age).ThenByAscending(p => p.Name); - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + _changeComparer.OnNext(comparer); - var expectedResult = people.OrderBy(p => p, _comparer); - var actualResult = _results.Data.Items; + var expectedResult = people.OrderBy(p => p, comparer).ToArray(); + var actualResult = _results.Data.Items.ToArray(); + //actualResult.(expectedResult); actualResult.Should().BeEquivalentTo(expectedResult); } [Fact] - public void ChangeComparer() + public void SortInitialBatch() { var people = _generator.Take(100).ToArray(); _source.AddRange(people); - var newComparer = SortExpressionComparer.Ascending(p => p.Name) - .ThenByAscending(p => p.Age); - - _changeComparer.OnNext(newComparer); - - _results.Data.Count.Should().Be(100, "Should be 100 people in the cache"); + _results.Data.Count.Should().Be(100); - var expectedResult = people.OrderBy(p => p, newComparer); + var expectedResult = people.OrderBy(p => p, _comparer); var actualResult = _results.Data.Items; actualResult.Should().BeEquivalentTo(expectedResult); diff --git a/src/DynamicData.Tests/List/SortPrimitiveFixture.cs b/src/DynamicData.Tests/List/SortPrimitiveFixture.cs index fe36fc445..660602ec2 100644 --- a/src/DynamicData.Tests/List/SortPrimitiveFixture.cs +++ b/src/DynamicData.Tests/List/SortPrimitiveFixture.cs @@ -1,18 +1,22 @@ using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Binding; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { public class SortPrimitiveFixture : IDisposable { - private readonly ISourceList _source; + private readonly IComparer _comparer = SortExpressionComparer.Ascending(i => i); + private readonly ChangeSetAggregator _results; - private readonly IComparer _comparer = SortExpressionComparer.Ascending(i => i); + private readonly ISourceList _source; public SortPrimitiveFixture() { @@ -30,7 +34,7 @@ public void Dispose() public void RemoveRandomSorts() { //seems an odd test but believe me it catches exceptions when sorting on primitives - var items = Enumerable.Range(1,100).OrderBy(_=>Guid.NewGuid()).ToArray(); + var items = Enumerable.Range(1, 100).OrderBy(_ => Guid.NewGuid()).ToArray(); _source.AddRange(items); _results.Data.Count.Should().Be(100); @@ -44,8 +48,6 @@ public void RemoveRandomSorts() { _source.Remove(i); } - } - } } \ No newline at end of file diff --git a/src/DynamicData.Tests/List/SourceListPreviewFixture.cs b/src/DynamicData.Tests/List/SourceListPreviewFixture.cs index e373f1d63..a2d40e5ac 100644 --- a/src/DynamicData.Tests/List/SourceListPreviewFixture.cs +++ b/src/DynamicData.Tests/List/SourceListPreviewFixture.cs @@ -1,5 +1,6 @@ using System; using System.Linq; + using Xunit; namespace DynamicData.Tests.List @@ -13,87 +14,18 @@ public SourceListPreviewFixture() _source = new SourceList(); } - public void Dispose() - { - _source.Dispose(); - } - - [Fact] - public void NoChangesAllowedDuringPreview() - { - // On preview, try adding an arbitrary item - var d = _source.Preview().Subscribe(_ => - { - Assert.Throws(() => _source.Add(1)); - }); - - // Trigger a change - _source.Add(1); - - // Cleanup - d.Dispose(); - } - - [Fact] - public void RecursiveEditsWork() - { - _source.Edit(l => - { - _source.Edit(l2 => l2.Add(1)); - Assert.True(_source.Items.SequenceEqual(new[] { 1 })); - Assert.True(l.SequenceEqual(new[] { 1 })); - }); - - Assert.True(_source.Items.SequenceEqual(new []{1})); - } - - [Fact] - public void RecursiveEditsHavePostponedEvents() - { - var preview = _source.Preview().AsAggregator(); - var connect = _source.Connect().AsAggregator(); - _source.Edit(l => - { - _source.Edit(l2 => l2.Add(1)); - Assert.Equal(0, preview.Messages.Count); - Assert.Equal(0, connect.Messages.Count); - }); - - Assert.Equal(1, preview.Messages.Count); - Assert.Equal(1, connect.Messages.Count); - - Assert.True(_source.Items.SequenceEqual(new[] { 1 })); - } - - [Fact] - public void PreviewEventsAreCorrect() - { - var preview = _source.Preview().AsAggregator(); - var connect = _source.Connect().AsAggregator(); - _source.Edit(l => - { - l.Add(1); - _source.Edit(l2 => l2.Add(2)); - l.Remove(2); - l.AddRange(new []{3, 4, 5}); - l.Move(1, 0); - }); - - Assert.True(preview.Messages.SequenceEqual(connect.Messages)); - Assert.True(_source.Items.SequenceEqual(new[] { 3, 1, 4, 5 })); - } - [Fact] public void ChangesAreNotYetAppliedDuringPreview() { _source.Clear(); // On preview, make sure the list is empty - var d = _source.Preview().Subscribe(_ => - { - Assert.True(_source.Count == 0); - Assert.True(_source.Items.Count() == 0); - }); + var d = _source.Preview().Subscribe( + _ => + { + Assert.True(_source.Count == 0); + Assert.True(_source.Items.Count() == 0); + }); // Trigger a change _source.Add(1); @@ -108,7 +40,7 @@ public void ConnectPreviewPredicateIsApplied() _source.Clear(); // Collect preview messages about even numbers only - var aggregator = _source.Preview(i => i % 2 == 0) .AsAggregator(); + var aggregator = _source.Preview(i => i % 2 == 0).AsAggregator(); // Trigger changes _source.Add(1); @@ -123,18 +55,23 @@ public void ConnectPreviewPredicateIsApplied() aggregator.Dispose(); } + public void Dispose() + { + _source.Dispose(); + } + [Fact] public void FormNewListFromChanges() { _source.Clear(); - _source.AddRange(Enumerable.Range(1,100)); + _source.AddRange(Enumerable.Range(1, 100)); // Collect preview messages about even numbers only var aggregator = _source.Preview(i => i % 2 == 0).AsAggregator(); _source.RemoveAt(10); - _source.RemoveRange(10,5); + _source.RemoveRange(10, 5); // Trigger changes _source.Add(1); _source.Add(2); @@ -147,5 +84,70 @@ public void FormNewListFromChanges() // Cleanup aggregator.Dispose(); } + + [Fact] + public void NoChangesAllowedDuringPreview() + { + // On preview, try adding an arbitrary item + var d = _source.Preview().Subscribe(_ => { Assert.Throws(() => _source.Add(1)); }); + + // Trigger a change + _source.Add(1); + + // Cleanup + d.Dispose(); + } + + [Fact] + public void PreviewEventsAreCorrect() + { + var preview = _source.Preview().AsAggregator(); + var connect = _source.Connect().AsAggregator(); + _source.Edit( + l => + { + l.Add(1); + _source.Edit(l2 => l2.Add(2)); + l.Remove(2); + l.AddRange(new[] { 3, 4, 5 }); + l.Move(1, 0); + }); + + Assert.True(preview.Messages.SequenceEqual(connect.Messages)); + Assert.True(_source.Items.SequenceEqual(new[] { 3, 1, 4, 5 })); + } + + [Fact] + public void RecursiveEditsHavePostponedEvents() + { + var preview = _source.Preview().AsAggregator(); + var connect = _source.Connect().AsAggregator(); + _source.Edit( + l => + { + _source.Edit(l2 => l2.Add(1)); + Assert.Equal(0, preview.Messages.Count); + Assert.Equal(0, connect.Messages.Count); + }); + + Assert.Equal(1, preview.Messages.Count); + Assert.Equal(1, connect.Messages.Count); + + Assert.True(_source.Items.SequenceEqual(new[] { 1 })); + } + + [Fact] + public void RecursiveEditsWork() + { + _source.Edit( + l => + { + _source.Edit(l2 => l2.Add(1)); + Assert.True(_source.Items.SequenceEqual(new[] { 1 })); + Assert.True(l.SequenceEqual(new[] { 1 })); + }); + + Assert.True(_source.Items.SequenceEqual(new[] { 1 })); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/SubscribeManyFixture.cs b/src/DynamicData.Tests/List/SubscribeManyFixture.cs index 91a6b8754..032049fc7 100644 --- a/src/DynamicData.Tests/List/SubscribeManyFixture.cs +++ b/src/DynamicData.Tests/List/SubscribeManyFixture.cs @@ -1,53 +1,29 @@ using System; using System.Linq; using System.Reactive.Disposables; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class SubscribeManyFixture: IDisposable + public class SubscribeManyFixture : IDisposable { - private class SubscribeableObject - { - public bool IsSubscribed { get; private set; } - private int Id { get; } - - public void Subscribe() - { - IsSubscribed = true; - } - - public void UnSubscribe() - { - IsSubscribed = false; - } - - public SubscribeableObject(int id) - { - Id = id; - } - } + private readonly ChangeSetAggregator _results; private readonly ISourceList _source; - private readonly ChangeSetAggregator _results; - public SubscribeManyFixture() + public SubscribeManyFixture() { _source = new SourceList(); _results = new ChangeSetAggregator( - _source.Connect().SubscribeMany(subscribeable => - { - subscribeable.Subscribe(); - return Disposable.Create(subscribeable.UnSubscribe); - })); - } - - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); + _source.Connect().SubscribeMany( + subscribeable => + { + subscribeable.Subscribe(); + return Disposable.Create(subscribeable.UnSubscribe); + })); } [Fact] @@ -60,15 +36,10 @@ public void AddedItemWillbeSubscribed() _results.Data.Items.First().IsSubscribed.Should().Be(true, "Should be subscribed"); } - [Fact] - public void RemoveIsUnsubscribed() + public void Dispose() { - _source.Add(new SubscribeableObject(1)); - _source.RemoveAt(0); - - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Data.Count.Should().Be(0, "Should be 0 items in the cache"); - _results.Messages[1].First().Item.Current.IsSubscribed.Should().Be(false, "Should be be unsubscribed"); + _source.Dispose(); + _results.Dispose(); } //[Fact] @@ -95,5 +66,38 @@ public void EverythingIsUnsubscribedWhenStreamIsDisposed() items.All(d => !d.IsSubscribed).Should().BeTrue(); } + + [Fact] + public void RemoveIsUnsubscribed() + { + _source.Add(new SubscribeableObject(1)); + _source.RemoveAt(0); + + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Data.Count.Should().Be(0, "Should be 0 items in the cache"); + _results.Messages[1].First().Item.Current.IsSubscribed.Should().Be(false, "Should be be unsubscribed"); + } + + private class SubscribeableObject + { + public SubscribeableObject(int id) + { + Id = id; + } + + public bool IsSubscribed { get; private set; } + + private int Id { get; } + + public void Subscribe() + { + IsSubscribed = true; + } + + public void UnSubscribe() + { + IsSubscribed = false; + } + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/SwitchFixture.cs b/src/DynamicData.Tests/List/SwitchFixture.cs index 77a2b7951..5d71868c3 100644 --- a/src/DynamicData.Tests/List/SwitchFixture.cs +++ b/src/DynamicData.Tests/List/SwitchFixture.cs @@ -1,43 +1,28 @@ - -using System; +using System; using System.Linq; using System.Reactive.Subjects; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class SwitchFixture: IDisposable + public class SwitchFixture : IDisposable { - private readonly ISubject> _switchable; - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public SwitchFixture() + private readonly ISourceList _source; + + private readonly ISubject> _switchable; + + public SwitchFixture() { _source = new SourceList(); _switchable = new BehaviorSubject>(_source); _results = _switchable.Switch().AsAggregator(); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - - [Fact] - public void PoulatesFirstSource() - { - var inital = Enumerable.Range(1,100).ToArray(); - _source.AddRange(inital); - - _results.Data.Count.Should().Be(100); - - inital.Should().BeEquivalentTo(_source.Items); - } - [Fact] public void ClearsForNewSource() { @@ -57,8 +42,23 @@ public void ClearsForNewSource() var nextUpdates = Enumerable.Range(100, 100).ToArray(); newSource.AddRange(nextUpdates); _results.Data.Count.Should().Be(200); + } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); } + [Fact] + public void PoulatesFirstSource() + { + var inital = Enumerable.Range(1, 100).ToArray(); + _source.AddRange(inital); + + _results.Data.Count.Should().Be(100); + + inital.Should().BeEquivalentTo(_source.Items); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/ToObservableChangeSetFixture.cs b/src/DynamicData.Tests/List/ToObservableChangeSetFixture.cs index 0fabb2cf8..6acdcae14 100644 --- a/src/DynamicData.Tests/List/ToObservableChangeSetFixture.cs +++ b/src/DynamicData.Tests/List/ToObservableChangeSetFixture.cs @@ -1,38 +1,40 @@ -using DynamicData.Tests.Domain; -using Microsoft.Reactive.Testing; -using Xunit; -using System; +using System; using System.Collections.Generic; + +using DynamicData.Tests.Domain; + using FluentAssertions; +using Microsoft.Reactive.Testing; + +using Xunit; + namespace DynamicData.Tests.List { - public class ToObservableChangeSetFixture : ReactiveTest, IDisposable { - private IObservable _observable; - private readonly TestScheduler _scheduler; private readonly IDisposable _disposable; + + private readonly Person _person1 = new("One", 1); + + private readonly Person _person2 = new("Two", 2); + + private readonly Person _person3 = new("Three", 3); + + private readonly TestScheduler _scheduler; + private readonly List _target; - private readonly Person _person1 = new Person("One", 1); - private readonly Person _person2 = new Person("Two", 2); - private readonly Person _person3 = new Person("Three", 3); + private readonly IObservable _observable; - public ToObservableChangeSetFixture() + public ToObservableChangeSetFixture() { _scheduler = new TestScheduler(); - _observable = _scheduler.CreateColdObservable( - OnNext(1, _person1), - OnNext(2, _person2), - OnNext(3, _person3)); + _observable = _scheduler.CreateColdObservable(OnNext(1, _person1), OnNext(2, _person2), OnNext(3, _person3)); _target = new List(); - _disposable = _observable - .ToObservableChangeSet(2, _scheduler) - .Clone(_target) - .Subscribe(); + _disposable = _observable.ToObservableChangeSet(2, _scheduler).Clone(_target).Subscribe(); } public void Dispose() @@ -49,9 +51,9 @@ public void ShouldLimitSizeOfBoundCollection() _scheduler.AdvanceTo(3); _target.Count.Should().Be(2, "Should be 2 item in target collection because of size limit"); - var expected = new[] {_person2, _person3}; + var expected = new[] { _person2, _person3 }; _target.Should().BeEquivalentTo(expected); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/TransformAsyncFixture.cs b/src/DynamicData.Tests/List/TransformAsyncFixture.cs index 9aca9fa8d..82dd43add 100644 --- a/src/DynamicData.Tests/List/TransformAsyncFixture.cs +++ b/src/DynamicData.Tests/List/TransformAsyncFixture.cs @@ -1,24 +1,25 @@ using System; using System.Threading.Tasks; + using DynamicData.Tests.Domain; namespace DynamicData.Tests.List { - [Obsolete("Not obsolete - test commented out due to test run freezing on Appveyor")] - public class TransformAsyncFixture: IDisposable + public class TransformAsyncFixture : IDisposable { - private ISourceList _source; - private ChangeSetAggregator _results; - private readonly Func> _transformFactory = p => - { - var gender = p.Age % 2 == 0 ? "M" : "F"; - var transformed = new PersonWithGender(p, gender); - return Task.FromResult(transformed); - }; + { + var gender = p.Age % 2 == 0 ? "M" : "F"; + var transformed = new PersonWithGender(p, gender); + return Task.FromResult(transformed); + }; + + private readonly ChangeSetAggregator _results; + + private readonly ISourceList _source; - public TransformAsyncFixture() + public TransformAsyncFixture() { _source = new SourceList(); _results = new ChangeSetAggregator(_source.Connect().TransformAsync(_transformFactory)); diff --git a/src/DynamicData.Tests/List/TransformFixture.cs b/src/DynamicData.Tests/List/TransformFixture.cs index 569dbc991..a8dbdd517 100644 --- a/src/DynamicData.Tests/List/TransformFixture.cs +++ b/src/DynamicData.Tests/List/TransformFixture.cs @@ -1,35 +1,32 @@ using System; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class TransformFixture: IDisposable + public class TransformFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; + private readonly ISourceList _source; + private readonly Func _transformFactory = p => - { - string gender = p.Age % 2 == 0 ? "M" : "F"; - return new PersonWithGender(p, gender); - }; + { + string gender = p.Age % 2 == 0 ? "M" : "F"; + return new PersonWithGender(p, gender); + }; - public TransformFixture() + public TransformFixture() { _source = new SourceList(); _results = new ChangeSetAggregator(_source.Connect().Transform(_transformFactory)); } - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); - } - [Fact] public void Add() { @@ -41,6 +38,40 @@ public void Add() _results.Data.Items.First().Should().Be(_transformFactory(person), "Should be same person"); } + [Fact] + public void BatchOfUniqueUpdates() + { + var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); + + _source.AddRange(people); + + _results.Messages.Count.Should().Be(1, "Should be 1 updates"); + _results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); + + var transformed = people.Select(_transformFactory).OrderBy(p => p.Age).ToArray(); + _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(transformed, "Incorrect transform result"); + } + + [Fact] + public void Clear() + { + var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); + + _source.AddRange(people); + _source.Clear(); + + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(100, "Should be 80 addes"); + _results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); + _results.Data.Count.Should().Be(0, "Should be nothing cached"); + } + + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } + [Fact] public void Remove() { @@ -74,35 +105,6 @@ public void RemoveWithoutIndex() results.Data.Count.Should().Be(0, "Should be nothing cached"); } - [Fact] - public void Update() - { - const string key = "Adult1"; - var newperson = new Person(key, 50); - var updated = new Person(key, 51); - - _source.Add(newperson); - _source.Add(updated); - - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); - _results.Messages[0].Replaced.Should().Be(0, "Should be 1 update"); - } - - [Fact] - public void BatchOfUniqueUpdates() - { - var people = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); - - _source.AddRange(people); - - _results.Messages.Count.Should().Be(1, "Should be 1 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should return 100 adds"); - - var transformed = people.Select(_transformFactory).OrderBy(p => p.Age).ToArray(); - _results.Data.Items.OrderBy(p => p.Age).Should().BeEquivalentTo(transformed, "Incorrect transform result"); - } - [Fact] public void SameKeyChanges() { @@ -116,33 +118,31 @@ public void SameKeyChanges() } [Fact] - public void Clear() + public void TransformToNull() { - var people = Enumerable.Range(1, 100).Select(l => new Person("Name" + l, l)).ToArray(); - - _source.AddRange(people); - _source.Clear(); - - _results.Messages.Count.Should().Be(2, "Should be 2 updates"); - _results.Messages[0].Adds.Should().Be(100, "Should be 80 addes"); - _results.Messages[1].Removes.Should().Be(100, "Should be 80 removes"); - _results.Data.Count.Should().Be(0, "Should be nothing cached"); + using var source = new SourceList(); + using var results = new ChangeSetAggregator(source.Connect() + .Transform((Func)(p => null))); + source.Add(new Person("Adult1", 50)); + + results.Messages.Count.Should().Be(1, "Should be 1 updates"); + results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); + results.Data.Items.First().Should().Be(null, "Should be same person"); } [Fact] - public void TransformToNull() + public void Update() { - using (var source = new SourceList()) - using (var results = - new ChangeSetAggregator(source.Connect() - .Transform((Func) (p => null)))) - { - source.Add(new Person("Adult1", 50)); + const string key = "Adult1"; + var newperson = new Person(key, 50); + var updated = new Person(key, 51); - results.Messages.Count.Should().Be(1, "Should be 1 updates"); - results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); - results.Data.Items.First().Should().Be(null, "Should be same person"); - } + _source.Add(newperson); + _source.Add(updated); + + _results.Messages.Count.Should().Be(2, "Should be 2 updates"); + _results.Messages[0].Adds.Should().Be(1, "Should be 1 adds"); + _results.Messages[0].Replaced.Should().Be(0, "Should be 1 update"); } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/List/TransformManyFixture.cs b/src/DynamicData.Tests/List/TransformManyFixture.cs index 626b5bf83..5c581d755 100644 --- a/src/DynamicData.Tests/List/TransformManyFixture.cs +++ b/src/DynamicData.Tests/List/TransformManyFixture.cs @@ -1,82 +1,43 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using DynamicData.Tests.Domain; using DynamicData.Tests.Utilities; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class TransformManyFixture: IDisposable + public class TransformManyFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; - public TransformManyFixture() + private readonly ISourceList _source; + + public TransformManyFixture() { _source = new SourceList(); - _results = _source.Connect() - .TransformMany(p => p.Relations.RecursiveSelect(r => r.Relations)) - .AsAggregator(); - } - - public void Dispose() - { - _source.Dispose(); + _results = _source.Connect().TransformMany(p => p.Relations.RecursiveSelect(r => r.Relations)).AsAggregator(); } [Fact] public void Add() { var frientofchild1 = new PersonWithRelations("Friend1", 10); - var child1 = new PersonWithRelations("Child1", 10, new[] {frientofchild1}); + var child1 = new PersonWithRelations("Child1", 10, new[] { frientofchild1 }); var child2 = new PersonWithRelations("Child2", 8); var child3 = new PersonWithRelations("Child3", 8); - var mother = new PersonWithRelations("Mother", 35, new[] {child1, child2, child3}); + var mother = new PersonWithRelations("Mother", 35, new[] { child1, child2, child3 }); // var father = new PersonWithRelations("Father", 35, new[] {child1, child2, child3, mother}); _source.Add(mother); _results.Data.Count.Should().Be(4); - _results.Data.Items.Should().BeEquivalentTo(new[] { child1, child2, child3, frientofchild1 }); - } - - [Fact] - public void RemoveParent() - { - var frientofchild1 = new PersonWithRelations("Friend1", 10); - var child1 = new PersonWithRelations("Child1", 10, new[] {frientofchild1}); - var child2 = new PersonWithRelations("Child2", 8); - var child3 = new PersonWithRelations("Child3", 8); - var mother = new PersonWithRelations("Mother", 35, new[] {child1, child2, child3}); - - _source.Add(mother); - _source.Remove(mother); - _results.Data.Count.Should().Be(0); - - } - - [Fact] - public void Replace() - { - var frientofchild1 = new PersonWithRelations("Friend1", 10); - var child1 = new PersonWithRelations("Child1", 10, new[] {frientofchild1}); - var child2 = new PersonWithRelations("Child2", 8); - var child3 = new PersonWithRelations("Child3", 8); - var mother = new PersonWithRelations("Mother", 35, new[] {child1, child2, child3}); - - _source.Add(mother); - - var child4 = new PersonWithRelations("Child4", 2); - var updatedMother = new PersonWithRelations("Mother", 35, new[] {child1, child2, child4}); - - _source.Replace(mother, updatedMother); - - _results.Data.Count.Should().Be(4); - _results.Data.Items.Should().BeEquivalentTo(new[] { child1, child2, frientofchild1, child4 }); + _results.Data.Items.Should().BeEquivalentTo(child1, child2, child3, frientofchild1); } [Fact] @@ -98,33 +59,9 @@ public void AddRange() var child7 = new PersonWithRelations("Child7", 2); var anotherRelative2 = new PersonWithRelations("Another2", 2, new[] { child6, child7 }); - _source.AddRange(new[] {anotherRelative1, anotherRelative2}); + _source.AddRange(new[] { anotherRelative1, anotherRelative2 }); _results.Data.Count.Should().Be(8); - _results.Data.Items.Should().BeEquivalentTo(new[] { child1, child2, child3, frientofchild1, child4, child5, child6, child7 }); - - } - - [Fact] - public void RemoveRange() - { - var frientofchild1 = new PersonWithRelations("Friend1", 10); - var child1 = new PersonWithRelations("Child1", 10, new[] { frientofchild1 }); - var child2 = new PersonWithRelations("Child2", 8); - var child3 = new PersonWithRelations("Child3", 8); - var mother = new PersonWithRelations("Mother", 35, new[] { child1, child2, child3 }); - var child4 = new PersonWithRelations("Child4", 1); - var child5 = new PersonWithRelations("Child5", 2); - var anotherRelative1 = new PersonWithRelations("Another1", 2, new[] { child4, child5 }); - var child6 = new PersonWithRelations("Child6", 1); - var child7 = new PersonWithRelations("Child7", 2); - var anotherRelative2 = new PersonWithRelations("Another2", 2, new[] { child6, child7 }); - - _source.AddRange(new[] { mother, anotherRelative1, anotherRelative2 }); - - _source.RemoveRange(0,2); - _results.Data.Count.Should().Be(2); - _results.Data.Items.Should().BeEquivalentTo(new[] { child6, child7 }); - + _results.Data.Items.Should().BeEquivalentTo(child1, child2, child3, frientofchild1, child4, child5, child6, child7); } [Fact] @@ -146,7 +83,11 @@ public void Clear() _source.Clear(); _results.Data.Count.Should().Be(0); + } + public void Dispose() + { + _source.Dispose(); } [Fact] @@ -164,9 +105,8 @@ public void Move() _source.AddRange(new[] { anotherRelative1, anotherRelative2 }); _results.Messages.Count.Should().Be(1); - _source.Move(1,0); + _source.Move(1, 0); _results.Messages.Count.Should().Be(1); - } [Fact] @@ -174,10 +114,7 @@ public void Remove() { var tourProviders = new SourceList(); - var allTours = tourProviders - .Connect() - .TransformMany(tourProvider => tourProvider.Tours) - .AsObservableList(); + var allTours = tourProviders.Connect().TransformMany(tourProvider => tourProvider.Tours).AsObservableList(); var tour1_1 = new Tour("Tour 1.1"); var tour2_1 = new Tour("Tour 2.1"); @@ -190,33 +127,72 @@ public void Remove() tourProviders.AddRange(new[] { tp1, tp2, tp3 }); - allTours.Items.Should().BeEquivalentTo(new[] { tour1_1, tour2_1, tour2_2 }); + allTours.Items.Should().BeEquivalentTo(tour1_1, tour2_1, tour2_2); tp3.Tours.Add(tour3_1); - allTours.Items.Should().BeEquivalentTo(new[] { tour1_1, tour2_1, tour2_2, tour3_1 }); + allTours.Items.Should().BeEquivalentTo(tour1_1, tour2_1, tour2_2, tour3_1); tp2.Tours.Remove(tour2_1); - allTours.Items.Should().BeEquivalentTo(new[] { tour1_1, tour2_2, tour3_1 }); + allTours.Items.Should().BeEquivalentTo(tour1_1, tour2_2, tour3_1); tp2.Tours.Add(tour2_1); - allTours.Items.Should().BeEquivalentTo(new[] { tour1_1, tour2_1, tour2_2, tour3_1 }); + allTours.Items.Should().BeEquivalentTo(tour1_1, tour2_1, tour2_2, tour3_1); } - public class TourProvider + [Fact] + public void RemoveParent() { - public TourProvider(string name, IEnumerable tours) - { - Name = name; + var frientofchild1 = new PersonWithRelations("Friend1", 10); + var child1 = new PersonWithRelations("Child1", 10, new[] { frientofchild1 }); + var child2 = new PersonWithRelations("Child2", 8); + var child3 = new PersonWithRelations("Child3", 8); + var mother = new PersonWithRelations("Mother", 35, new[] { child1, child2, child3 }); - if (tours != null) - { - Tours.AddRange(tours); - } - } + _source.Add(mother); + _source.Remove(mother); + _results.Data.Count.Should().Be(0); + } - public string Name { get; } + [Fact] + public void RemoveRange() + { + var frientofchild1 = new PersonWithRelations("Friend1", 10); + var child1 = new PersonWithRelations("Child1", 10, new[] { frientofchild1 }); + var child2 = new PersonWithRelations("Child2", 8); + var child3 = new PersonWithRelations("Child3", 8); + var mother = new PersonWithRelations("Mother", 35, new[] { child1, child2, child3 }); + var child4 = new PersonWithRelations("Child4", 1); + var child5 = new PersonWithRelations("Child5", 2); + var anotherRelative1 = new PersonWithRelations("Another1", 2, new[] { child4, child5 }); + var child6 = new PersonWithRelations("Child6", 1); + var child7 = new PersonWithRelations("Child7", 2); + var anotherRelative2 = new PersonWithRelations("Another2", 2, new[] { child6, child7 }); - public ObservableCollection Tours { get; } = new ObservableCollection(); + _source.AddRange(new[] { mother, anotherRelative1, anotherRelative2 }); + + _source.RemoveRange(0, 2); + _results.Data.Count.Should().Be(2); + _results.Data.Items.Should().BeEquivalentTo(child6, child7); + } + + [Fact] + public void Replace() + { + var frientofchild1 = new PersonWithRelations("Friend1", 10); + var child1 = new PersonWithRelations("Child1", 10, new[] { frientofchild1 }); + var child2 = new PersonWithRelations("Child2", 8); + var child3 = new PersonWithRelations("Child3", 8); + var mother = new PersonWithRelations("Mother", 35, new[] { child1, child2, child3 }); + + _source.Add(mother); + + var child4 = new PersonWithRelations("Child4", 2); + var updatedMother = new PersonWithRelations("Mother", 35, new[] { child1, child2, child4 }); + + _source.Replace(mother, updatedMother); + + _results.Data.Count.Should().Be(4); + _results.Data.Items.Should().BeEquivalentTo(child1, child2, frientofchild1, child4); } public class Tour @@ -233,6 +209,22 @@ public override string ToString() return $"{nameof(Name)}: {Name}"; } } - } -} + public class TourProvider + { + public TourProvider(string name, IEnumerable? tours) + { + Name = name; + + if (tours is not null) + { + Tours.AddRange(tours); + } + } + + public string Name { get; } + + public ObservableCollection Tours { get; } = new ObservableCollection(); + } + } +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/TransformManyObservableCollectionFixture.cs b/src/DynamicData.Tests/List/TransformManyObservableCollectionFixture.cs index 2722e2e94..bf0fe331a 100644 --- a/src/DynamicData.Tests/List/TransformManyObservableCollectionFixture.cs +++ b/src/DynamicData.Tests/List/TransformManyObservableCollectionFixture.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List @@ -15,238 +18,216 @@ public void FlattenObservableCollection() var children = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); int childIndex = 0; - var parents = Enumerable.Range(1, 50) - .Select(i => - { - var parent = new Parent(i, new[] + var parents = Enumerable.Range(1, 50).Select( + i => { - children[childIndex], - children[childIndex + 1] - }); - - childIndex = childIndex + 2; - return parent; - }).ToArray(); - - using (var source = new SourceList()) - using (var aggregator = source.Connect() - .TransformMany(p => p.Children) - .AsAggregator()) - { - source.AddRange(parents); - - aggregator.Data.Count.Should().Be(100); - - //add a child to an observable collection and check the new item is added - parents[0].Children.Add(new Person("NewlyAddded", 100)); - aggregator.Data.Count.Should().Be(101); - - ////remove first parent and check children have gone - source.RemoveAt(0); - aggregator.Data.Count.Should().Be(98); - - //check items can be cleared and then added back in - var childrenInZero = parents[1].Children.ToArray(); - parents[1].Children.Clear(); - aggregator.Data.Count.Should().Be(96); - parents[1].Children.AddRange(childrenInZero); - aggregator.Data.Count.Should().Be(98); - - //replace produces an update - var replacedChild = parents[1].Children[0]; - var replacement = new Person("Replacement", 100); - parents[1].Children[0] = replacement; - aggregator.Data.Count.Should().Be(98); - - aggregator.Data.Items.Contains(replacement).Should().BeTrue(); - aggregator.Data.Items.Contains(replacedChild).Should().BeFalse(); - } + var parent = new Parent( + i, + new[] + { + children[childIndex], + children[childIndex + 1] + }); + + childIndex += 2; + return parent; + }).ToArray(); + + using var source = new SourceList(); + using var aggregator = source.Connect().TransformMany(p => p.Children).AsAggregator(); + source.AddRange(parents); + + aggregator.Data.Count.Should().Be(100); + + //add a child to an observable collection and check the new item is added + parents[0].Children.Add(new Person("NewlyAddded", 100)); + aggregator.Data.Count.Should().Be(101); + + ////remove first parent and check children have gone + source.RemoveAt(0); + aggregator.Data.Count.Should().Be(98); + + //check items can be cleared and then added back in + var childrenInZero = parents[1].Children.ToArray(); + parents[1].Children.Clear(); + aggregator.Data.Count.Should().Be(96); + parents[1].Children.AddRange(childrenInZero); + aggregator.Data.Count.Should().Be(98); + + //replace produces an update + var replacedChild = parents[1].Children[0]; + var replacement = new Person("Replacement", 100); + parents[1].Children[0] = replacement; + aggregator.Data.Count.Should().Be(98); + + aggregator.Data.Items.Contains(replacement).Should().BeTrue(); + aggregator.Data.Items.Contains(replacedChild).Should().BeFalse(); } [Fact] - public void FlattenReadOnlyObservableCollection() + public void FlattenObservableList() { var children = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); int childIndex = 0; - var parents = Enumerable.Range(1, 50) - .Select(i => - { - var parent = new Parent(i, new[] + var parents = Enumerable.Range(1, 50).Select( + i => { - children[childIndex], - children[childIndex + 1] - }); - - childIndex = childIndex + 2; - return parent; - }).ToArray(); - - using (var source = new SourceList()) - using (var aggregator = source.Connect() - .TransformMany(p => p.ChildrenReadonly) - .AsAggregator()) - { - source.AddRange(parents); - - aggregator.Data.Count.Should().Be(100); - - //add a child to an observable collection and check the new item is added - parents[0].Children.Add(new Person("NewlyAddded", 100)); - aggregator.Data.Count.Should().Be(101); - - ////remove first parent and check children have gone - source.RemoveAt(0); - aggregator.Data.Count.Should().Be(98); - - //check items can be cleared and then added back in - var childrenInZero = parents[1].Children.ToArray(); - parents[1].Children.Clear(); - aggregator.Data.Count.Should().Be(96); - parents[1].Children.AddRange(childrenInZero); - aggregator.Data.Count.Should().Be(98); - - //replace produces an update - var replacedChild = parents[1].Children[0]; - var replacement = new Person("Replacement", 100); - parents[1].Children[0] = replacement; - aggregator.Data.Count.Should().Be(98); - - //replace produces an update - aggregator.Data.Items.Contains(replacement).Should().BeTrue(); - aggregator.Data.Items.Contains(replacedChild).Should().BeFalse(); - } + var parent = new ParentDynamic( + i, + new[] + { + children[childIndex], + children[childIndex + 1] + }); + + childIndex += 2; + return parent; + }).ToArray(); + + using var source = new SourceList(); + using var aggregator = source.Connect().TransformMany(p => p.ChildrenObservable).AsAggregator(); + T at(IEnumerable elements, int i) => elements.Skip(i).First(); + + source.AddRange(parents); + + aggregator.Data.Count.Should().Be(100); + + //add a child to an observable collection and check the new item is added + parents[0].Children.Add(new Person("NewlyAddded", 100)); + aggregator.Data.Count.Should().Be(101); + + ////remove first parent and check children have gone + source.RemoveAt(0); + aggregator.Data.Count.Should().Be(98); + + //check items can be cleared and then added back in + var childrenInZero = parents[1].Children.Items.ToArray(); + parents[1].Children.Clear(); + aggregator.Data.Count.Should().Be(96); + parents[1].Children.AddRange(childrenInZero); + aggregator.Data.Count.Should().Be(98); + + //replace produces an update + var replacedChild = at(parents[1].Children.Items, 0); + var replacement = new Person("Replacement", 100); + parents[1].Children.ReplaceAt(0, replacement); + aggregator.Data.Count.Should().Be(98); + + //replace produces an update + aggregator.Data.Items.Contains(replacement).Should().BeTrue(); + aggregator.Data.Items.Contains(replacedChild).Should().BeFalse(); } [Fact] - public void FlattenObservableList() + public void FlattenReadOnlyObservableCollection() { var children = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); int childIndex = 0; - var parents = Enumerable.Range(1, 50) - .Select(i => - { - var parent = new ParentDynamic(i, new[] + var parents = Enumerable.Range(1, 50).Select( + i => { - children[childIndex], - children[childIndex + 1] - }); - - childIndex = childIndex + 2; - return parent; - }).ToArray(); - - using (var source = new SourceList()) - using (var aggregator = source.Connect() - .TransformMany(p => p.ChildrenObservable) - .AsAggregator()) - { - T at(IEnumerable elements, int i) => elements.Skip(i).First(); - - source.AddRange(parents); - - aggregator.Data.Count.Should().Be(100); - - //add a child to an observable collection and check the new item is added - parents[0].Children.Add(new Person("NewlyAddded", 100)); - aggregator.Data.Count.Should().Be(101); - - ////remove first parent and check children have gone - source.RemoveAt(0); - aggregator.Data.Count.Should().Be(98); - - //check items can be cleared and then added back in - var childrenInZero = parents[1].Children.Items.ToArray(); - parents[1].Children.Clear(); - aggregator.Data.Count.Should().Be(96); - parents[1].Children.AddRange(childrenInZero); - aggregator.Data.Count.Should().Be(98); - - //replace produces an update - var replacedChild = at(parents[1].Children.Items, 0); - var replacement = new Person("Replacement", 100); - parents[1].Children.ReplaceAt(0, replacement); - aggregator.Data.Count.Should().Be(98); - - //replace produces an update - aggregator.Data.Items.Contains(replacement).Should().BeTrue(); - aggregator.Data.Items.Contains(replacedChild).Should().BeFalse(); - } + var parent = new Parent( + i, + new[] + { + children[childIndex], + children[childIndex + 1] + }); + + childIndex += 2; + return parent; + }).ToArray(); + + using var source = new SourceList(); + using var aggregator = source.Connect().TransformMany(p => p.ChildrenReadonly).AsAggregator(); + source.AddRange(parents); + + aggregator.Data.Count.Should().Be(100); + + //add a child to an observable collection and check the new item is added + parents[0].Children.Add(new Person("NewlyAddded", 100)); + aggregator.Data.Count.Should().Be(101); + + ////remove first parent and check children have gone + source.RemoveAt(0); + aggregator.Data.Count.Should().Be(98); + + //check items can be cleared and then added back in + var childrenInZero = parents[1].Children.ToArray(); + parents[1].Children.Clear(); + aggregator.Data.Count.Should().Be(96); + parents[1].Children.AddRange(childrenInZero); + aggregator.Data.Count.Should().Be(98); + + //replace produces an update + var replacedChild = parents[1].Children[0]; + var replacement = new Person("Replacement", 100); + parents[1].Children[0] = replacement; + aggregator.Data.Count.Should().Be(98); + + //replace produces an update + aggregator.Data.Items.Contains(replacement).Should().BeTrue(); + aggregator.Data.Items.Contains(replacedChild).Should().BeFalse(); } [Fact] public void ObservableCollectionWithoutInitialData() { - using (var parents = new SourceList()) - { - - var collection = parents.Connect() - .TransformMany(d => d.Children) - .AsObservableList(); + using var parents = new SourceList(); + var collection = parents.Connect().TransformMany(d => d.Children).AsObservableList(); - var parent = new Parent(); - parents.Add(parent); + var parent = new Parent(); + parents.Add(parent); - collection.Count.Should().Be(0); + collection.Count.Should().Be(0); - parent.Children.Add(new Person("child1", 1)); - collection.Count.Should().Be(1); + parent.Children.Add(new Person("child1", 1)); + collection.Count.Should().Be(1); - parent.Children.Add(new Person("child2", 2)); - collection.Count.Should().Be(2); - } + parent.Children.Add(new Person("child2", 2)); + collection.Count.Should().Be(2); } [Fact] - public void ReadOnlyObservableCollectionWithoutInitialData() + public void ObservableListWithoutInitialData() { - using (var parents = new SourceList()) - { - var collection = parents.Connect() - .TransformMany(d => d.ChildrenReadonly) - .AsObservableList(); + using var parents = new SourceList(); + var collection = parents.Connect().TransformMany(d => d.ChildrenObservable).AsObservableList(); - var parent = new Parent(); - parents.Add(parent); + var parent = new ParentDynamic(); + parents.Add(parent); - collection.Count.Should().Be(0); + collection.Count.Should().Be(0); - parent.Children.Add(new Person("child1", 1)); - collection.Count.Should().Be(1); + parent.Children.Add(new Person("child1", 1)); + collection.Count.Should().Be(1); - parent.Children.Add(new Person("child2", 2)); - collection.Count.Should().Be(2); - } + parent.Children.Add(new Person("child2", 2)); + collection.Count.Should().Be(2); } [Fact] - public void ObservableListWithoutInitialData() + public void ReadOnlyObservableCollectionWithoutInitialData() { - using (var parents = new SourceList()) - { - var collection = parents.Connect() - .TransformMany(d => d.ChildrenObservable) - .AsObservableList(); + using var parents = new SourceList(); + var collection = parents.Connect().TransformMany(d => d.ChildrenReadonly).AsObservableList(); - var parent = new ParentDynamic(); - parents.Add(parent); + var parent = new Parent(); + parents.Add(parent); - collection.Count.Should().Be(0); + collection.Count.Should().Be(0); - parent.Children.Add(new Person("child1", 1)); - collection.Count.Should().Be(1); + parent.Children.Add(new Person("child1", 1)); + collection.Count.Should().Be(1); - parent.Children.Add(new Person("child2", 2)); - collection.Count.Should().Be(2); - } + parent.Children.Add(new Person("child2", 2)); + collection.Count.Should().Be(2); } private class Parent { - public ObservableCollection Children { get; } - public ReadOnlyObservableCollection ChildrenReadonly { get; } - public Parent(int id, IEnumerable children) { Children = new ObservableCollection(children); @@ -258,13 +239,14 @@ public Parent() Children = new ObservableCollection(); ChildrenReadonly = new ReadOnlyObservableCollection(Children); } + + public ObservableCollection Children { get; } + + public ReadOnlyObservableCollection ChildrenReadonly { get; } } private class ParentDynamic { - public SourceList Children { get; } - public IObservableList ChildrenObservable { get; } - public ParentDynamic(int id, IEnumerable children) { Children = new SourceList(); @@ -277,6 +259,10 @@ public ParentDynamic() Children = new SourceList(); ChildrenObservable = Children.AsObservableList(); } + + public SourceList Children { get; } + + public IObservableList ChildrenObservable { get; } } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/TransformManyProjectionFixture.cs b/src/DynamicData.Tests/List/TransformManyProjectionFixture.cs index feafdb5ca..cfbc004b9 100644 --- a/src/DynamicData.Tests/List/TransformManyProjectionFixture.cs +++ b/src/DynamicData.Tests/List/TransformManyProjectionFixture.cs @@ -1,54 +1,48 @@ -using DynamicData.Aggregation; -using DynamicData.Binding; -using FluentAssertions; -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text; + +using DynamicData.Binding; + +using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class TransformManyProjectionFixture : IDisposable - { - private readonly ISourceList _source; - private readonly IObservableList _results; - - public TransformManyProjectionFixture() - { - _source = new SourceList(); - - _results = _source.Connect() - .AutoRefreshOnObservable(self => self.Children.ToObservableChangeSet()) - .TransformMany(parent => parent.Children.Select(c => new ProjectedNestedChild(parent, c)), new ProjectNestedChildEqualityComparer()) - .AsObservableList(); - } - - public void Dispose() - { - _source.Dispose(); + public class TransformManyProjectionFixture : IDisposable + { + private readonly IObservableList _results; + + private readonly ISourceList _source; + + public TransformManyProjectionFixture() + { + _source = new SourceList(); + + _results = _source.Connect().AutoRefreshOnObservable(self => self.Children.ToObservableChangeSet()).TransformMany(parent => parent.Children.Select(c => new ProjectedNestedChild(parent, c)), new ProjectNestedChildEqualityComparer()).AsObservableList(); } [Fact] public void AddRange() { var children = new[] - { - new NestedChild("A", "ValueA"), - new NestedChild("B", "ValueB"), - new NestedChild("C", "ValueC"), - new NestedChild("D", "ValueD"), - new NestedChild("E", "ValueE"), - new NestedChild("F", "ValueF") - }; + { + new NestedChild("A", "ValueA"), + new NestedChild("B", "ValueB"), + new NestedChild("C", "ValueC"), + new NestedChild("D", "ValueD"), + new NestedChild("E", "ValueE"), + new NestedChild("F", "ValueF") + }; var parents = new[] - { - new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), - new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), - new ClassWithNestedObservableCollection(3, new[] { children[4] }) - }; + { + new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), + new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), + new ClassWithNestedObservableCollection(3, new[] { children[4] }) + }; _source.AddRange(parents); @@ -56,25 +50,58 @@ public void AddRange() _results.Items.Should().BeEquivalentTo(parents.SelectMany(p => p.Children.Take(5).Select(c => new ProjectedNestedChild(p, c)))); } + public void Dispose() + { + _source.Dispose(); + } + + [Fact] + public void RemoveChild() + { + var children = new[] + { + new NestedChild("A", "ValueA"), + new NestedChild("B", "ValueB"), + new NestedChild("C", "ValueC"), + new NestedChild("D", "ValueD"), + new NestedChild("E", "ValueE"), + new NestedChild("F", "ValueF") + }; + + var parents = new[] + { + new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), + new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), + new ClassWithNestedObservableCollection(3, new[] { children[4] }) + }; + + _source.AddRange(parents); + + //remove a child + parents[1].Children.Remove(children[3]); + _results.Count.Should().Be(4); + _results.Items.Should().BeEquivalentTo(parents.SelectMany(p => p.Children.Where(child => child.Name != "D").Select(c => new ProjectedNestedChild(p, c)))); + } + [Fact] public void RemoveParent() { var children = new[] - { - new NestedChild("A", "ValueA"), - new NestedChild("B", "ValueB"), - new NestedChild("C", "ValueC"), - new NestedChild("D", "ValueD"), - new NestedChild("E", "ValueE"), - new NestedChild("F", "ValueF") - }; + { + new NestedChild("A", "ValueA"), + new NestedChild("B", "ValueB"), + new NestedChild("C", "ValueC"), + new NestedChild("D", "ValueD"), + new NestedChild("E", "ValueE"), + new NestedChild("F", "ValueF") + }; var parents = new[] - { - new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), - new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), - new ClassWithNestedObservableCollection(3, new[] { children[4] }) - }; + { + new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), + new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), + new ClassWithNestedObservableCollection(3, new[] { children[4] }) + }; _source.AddRange(parents); @@ -84,52 +111,50 @@ public void RemoveParent() _results.Items.Should().BeEquivalentTo(parents.Skip(1).SelectMany(p => p.Children.Select(c => new ProjectedNestedChild(p, c)))); } - [Fact] - public void RemoveChild() + private class ClassWithNestedObservableCollection { - var children = new[] + public ClassWithNestedObservableCollection(int id, IEnumerable animals) { - new NestedChild("A", "ValueA"), - new NestedChild("B", "ValueB"), - new NestedChild("C", "ValueC"), - new NestedChild("D", "ValueD"), - new NestedChild("E", "ValueE"), - new NestedChild("F", "ValueF") - }; + Id = id; + Children = new ObservableCollection(animals); + } - var parents = new[] + public ObservableCollection Children { get; } + + public int Id { get; } + } + + private class NestedChild + { + public NestedChild(string name, string value) { - new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), - new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), - new ClassWithNestedObservableCollection(3, new[] { children[4] }) - }; + Name = name; + Value = value; + } - _source.AddRange(parents); + public string Name { get; } - //remove a child - parents[1].Children.Remove(children[3]); - _results.Count.Should().Be(4); - _results.Items.Should().BeEquivalentTo(parents.SelectMany(p => p.Children.Where(child => child.Name != "D").Select(c => new ProjectedNestedChild(p, c)))); + public string Value { get; } } private class ProjectedNestedChild { - public ClassWithNestedObservableCollection Parent { get; } - - public NestedChild Child { get; } - public ProjectedNestedChild(ClassWithNestedObservableCollection parent, NestedChild child) { Parent = parent; Child = child; } + + public NestedChild Child { get; } + + public ClassWithNestedObservableCollection Parent { get; } } private class ProjectNestedChildEqualityComparer : IEqualityComparer { - public bool Equals(ProjectedNestedChild x, ProjectedNestedChild y) + public bool Equals(ProjectedNestedChild? x, ProjectedNestedChild? y) { - if (x == null || y == null) + if (x is null || y is null) return false; return x.Child.Name == y.Child.Name; @@ -140,29 +165,5 @@ public int GetHashCode(ProjectedNestedChild obj) return obj.Child.Name.GetHashCode(); } } - - private class NestedChild - { - public string Name { get; } - public string Value { get; } - - public NestedChild(string name, string value) - { - Name = name; - Value = value; - } - } - - private class ClassWithNestedObservableCollection - { - public int Id { get; } - public ObservableCollection Children { get; } - - public ClassWithNestedObservableCollection(int id, IEnumerable animals) - { - Id = id; - Children = new ObservableCollection(animals); - } - } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/TransformManyRefreshFixture.cs b/src/DynamicData.Tests/List/TransformManyRefreshFixture.cs index 79edaf7d2..4592532b3 100644 --- a/src/DynamicData.Tests/List/TransformManyRefreshFixture.cs +++ b/src/DynamicData.Tests/List/TransformManyRefreshFixture.cs @@ -1,31 +1,26 @@ +using System; +using System.Collections.Generic; + using DynamicData.Tests.Domain; using DynamicData.Tests.Utilities; + using FluentAssertions; -using System; -using System.Collections.Generic; + using Xunit; namespace DynamicData.Tests.List { public class TransformManyRefreshFixture : IDisposable { - private readonly ISourceList _source; private readonly ChangeSetAggregator _results; + private readonly ISourceList _source; + public TransformManyRefreshFixture() { _source = new SourceList(); - _results = _source.Connect() - .AutoRefresh() - .TransformMany(p => p.Friends.RecursiveSelect(r => r.Friends)) - .AsAggregator(); - } - - public void Dispose() - { - _source.Dispose(); - _results.Dispose(); + _results = _source.Connect().AutoRefresh().TransformMany(p => p.Friends.RecursiveSelect(r => r.Friends)).AsAggregator(); } [Fact] @@ -37,10 +32,10 @@ public void AutoRefresh() var person = new PersonWithFriends("Person", 50); _source.Add(person); - person.Friends = new[] {friend1, friend2}; + person.Friends = new[] { friend1, friend2 }; _results.Data.Count.Should().Be(2, "Should be 2 in the cache"); - _results.Data.Items.Should().BeEquivalentTo(new[] {friend1, friend2}); + _results.Data.Items.Should().BeEquivalentTo(friend1, friend2); } [Fact] @@ -48,7 +43,7 @@ public void AutoRefreshOnOtherProperty() { var friend1 = new PersonWithFriends("Friend1", 40); var friend2 = new PersonWithFriends("Friend2", 45); - var friends = new List {friend1}; + var friends = new List { friend1 }; var person = new PersonWithFriends("Person", 50, friends); _source.Add(person); @@ -56,7 +51,7 @@ public void AutoRefreshOnOtherProperty() person.Age = 55; _results.Data.Count.Should().Be(2, "Should be 2 in the cache"); - _results.Data.Items.Should().BeEquivalentTo(new[] {friend1, friend2}); + _results.Data.Items.Should().BeEquivalentTo(friend1, friend2); } [Fact] @@ -64,17 +59,22 @@ public void AutoRefreshRecursive() { var friend1 = new PersonWithFriends("Friend1", 30); var friend2 = new PersonWithFriends("Friend2", 35); - var friend3 = new PersonWithFriends("Friend3", 40, new[] {friend1}); - var friend4 = new PersonWithFriends("Friend4", 45, new[] {friend2}); + var friend3 = new PersonWithFriends("Friend3", 40, new[] { friend1 }); + var friend4 = new PersonWithFriends("Friend4", 45, new[] { friend2 }); - var person = new PersonWithFriends("Person", 50, new[] {friend3}); + var person = new PersonWithFriends("Person", 50, new[] { friend3 }); _source.Add(person); - person.Friends = new[] {friend4}; + person.Friends = new[] { friend4 }; _results.Data.Count.Should().Be(2, "Should be 2 in the cache"); - _results.Data.Items.Should().BeEquivalentTo(new[] {friend4, friend2}); + _results.Data.Items.Should().BeEquivalentTo(friend4, friend2); } + public void Dispose() + { + _source.Dispose(); + _results.Dispose(); + } } } \ No newline at end of file diff --git a/src/DynamicData.Tests/List/VirtualisationFixture.cs b/src/DynamicData.Tests/List/VirtualisationFixture.cs index a507ccffb..db87da8f6 100644 --- a/src/DynamicData.Tests/List/VirtualisationFixture.cs +++ b/src/DynamicData.Tests/List/VirtualisationFixture.cs @@ -1,19 +1,24 @@ using System; using System.Linq; using System.Reactive.Subjects; + using DynamicData.Tests.Domain; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - - public class VirtualisationFixture: IDisposable + public class VirtualisationFixture : IDisposable { - private readonly ISourceList _source; + private readonly RandomPersonGenerator _generator = new(); + + private readonly ISubject _requestSubject = new BehaviorSubject(new VirtualRequest(0, 25)); + private readonly ChangeSetAggregator _results; - private readonly ISubject _requestSubject = new BehaviorSubject(new VirtualRequest(0, 25)); - private readonly RandomPersonGenerator _generator = new RandomPersonGenerator(); + + private readonly ISourceList _source; public VirtualisationFixture() { @@ -28,16 +33,34 @@ public void Dispose() } [Fact] - public void VirtualiseInitial() + public void InsertAfterPageProducesNothing() { var people = _generator.Take(100).ToArray(); _source.AddRange(people); var expected = people.Take(25).ToArray(); + _source.InsertRange(_generator.Take(100), 50); _results.Data.Items.Should().BeEquivalentTo(expected); } + [Fact] + public void InsertInPageReflectsChange() + { + var people = _generator.Take(100).ToArray(); + _source.AddRange(people); + + var newPerson = new Person("A", 1); + _source.Insert(10, newPerson); + + var message = _results.Messages[1].ElementAt(0); + var removedPerson = people.ElementAt(24); + + _results.Data.Items.ElementAt(10).Should().Be(newPerson); + message.Item.Current.Should().Be(removedPerson); + message.Reason.Should().Be(ListChangeReason.Remove); + } + [Fact] public void MoveToNextPage() { @@ -50,32 +73,27 @@ public void MoveToNextPage() } [Fact] - public void InsertAfterPageProducesNothing() + public void MoveWithinSamePage() { var people = _generator.Take(100).ToArray(); _source.AddRange(people); + var personToMove = people[0]; + _source.Move(0, 10); - var expected = people.Take(25).ToArray(); - - _source.InsertRange(_generator.Take(100), 50); - _results.Data.Items.Should().BeEquivalentTo(expected); + var actualPersonAtIndex10 = _results.Data.Items.ElementAt(10); + actualPersonAtIndex10.Should().Be(personToMove); } [Fact] - public void InsertInPageReflectsChange() + public void MoveWithinSamePage2() { var people = _generator.Take(100).ToArray(); _source.AddRange(people); + var personToMove = people[10]; + _source.Move(10, 0); - var newPerson = new Person("A", 1); - _source.Insert(10, newPerson); - - var message = _results.Messages[1].ElementAt(0); - var removedPerson = people.ElementAt(24); - - _results.Data.Items.ElementAt(10).Should().Be(newPerson); - message.Item.Current.Should().Be(removedPerson); - message.Reason.Should().Be(ListChangeReason.Remove); + var actualPersonAtIndex0 = _results.Data.Items.ElementAt(0); + actualPersonAtIndex0.Should().Be(personToMove); } [Fact] @@ -101,27 +119,14 @@ public void RemoveBeforeShiftsPage() } [Fact] - public void MoveWithinSamePage() + public void VirtualiseInitial() { var people = _generator.Take(100).ToArray(); _source.AddRange(people); - var personToMove = people[0]; - _source.Move(0, 10); - var actualPersonAtIndex10 = _results.Data.Items.ElementAt(10); - actualPersonAtIndex10.Should().Be(personToMove); - } - - [Fact] - public void MoveWithinSamePage2() - { - var people = _generator.Take(100).ToArray(); - _source.AddRange(people); - var personToMove = people[10]; - _source.Move(10, 0); + var expected = people.Take(25).ToArray(); - var actualPersonAtIndex0 = _results.Data.Items.ElementAt(0); - actualPersonAtIndex0.Should().Be(personToMove); + _results.Data.Items.Should().BeEquivalentTo(expected); } } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/List/XOrFixture.cs b/src/DynamicData.Tests/List/XOrFixture.cs index b8139ca7f..01f3a3dd9 100644 --- a/src/DynamicData.Tests/List/XOrFixture.cs +++ b/src/DynamicData.Tests/List/XOrFixture.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; + using FluentAssertions; + using Xunit; namespace DynamicData.Tests.List { - public class XOrFixture : XOrFixtureBase { protected override IObservable> CreateObservable() @@ -24,10 +25,12 @@ protected override IObservable> CreateObservable() } } - public abstract class XOrFixtureBase: IDisposable + public abstract class XOrFixtureBase : IDisposable { protected ISourceList _source1; + protected ISourceList _source2; + private readonly ChangeSetAggregator _results; protected XOrFixtureBase() @@ -37,7 +40,24 @@ protected XOrFixtureBase() _results = CreateObservable().AsAggregator(); } - protected abstract IObservable> CreateObservable(); + [Fact] + public void ClearOnlyClearsOneSource() + { + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _source1.Clear(); + _results.Data.Count.Should().Be(5); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); + } + + [Fact] + public void CombineRange() + { + _source1.AddRange(Enumerable.Range(1, 5)); + _source2.AddRange(Enumerable.Range(6, 5)); + _results.Data.Count.Should().Be(10); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); + } public void Dispose() { @@ -63,6 +83,15 @@ public void NotIncludedWhenItemIsInTwoSources() _results.Data.Count.Should().Be(0); } + [Fact] + public void OverlappingRangeExludesInteresct() + { + _source1.AddRange(Enumerable.Range(1, 10)); + _source2.AddRange(Enumerable.Range(6, 10)); + _results.Data.Count.Should().Be(10); + _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 5).Union(Enumerable.Range(11, 5))); + } + [Fact] public void RemovedWhenNoLongerInBoth() { @@ -80,32 +109,6 @@ public void RemovedWhenNoLongerInEither() _results.Data.Count.Should().Be(0); } - [Fact] - public void CombineRange() - { - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _results.Data.Count.Should().Be(10); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); - } - - [Fact] - public void ClearOnlyClearsOneSource() - { - _source1.AddRange(Enumerable.Range(1, 5)); - _source2.AddRange(Enumerable.Range(6, 5)); - _source1.Clear(); - _results.Data.Count.Should().Be(5); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(6, 5)); - } - - [Fact] - public void OverlappingRangeExludesInteresct() - { - _source1.AddRange(Enumerable.Range(1, 10)); - _source2.AddRange(Enumerable.Range(6, 10)); - _results.Data.Count.Should().Be(10); - _results.Data.Items.Should().BeEquivalentTo(Enumerable.Range(1, 5).Union(Enumerable.Range(11, 5))); - } + protected abstract IObservable> CreateObservable(); } -} +} \ No newline at end of file diff --git a/src/DynamicData.Tests/Utilities/SelectManyExtensions.cs b/src/DynamicData.Tests/Utilities/SelectManyExtensions.cs index fc780573b..ba8f77d31 100644 --- a/src/DynamicData.Tests/Utilities/SelectManyExtensions.cs +++ b/src/DynamicData.Tests/Utilities/SelectManyExtensions.cs @@ -9,75 +9,50 @@ namespace DynamicData.Tests.Utilities /// public static class SelectManyExtensions { - public static IEnumerable SelectManyRecursive(this IEnumerable source, Func> selector) + public static IEnumerable EmptyIfNull(this IEnumerable source) { - if (source == null) - { - throw new ArgumentNullException("source"); - } - - if (selector == null) - { - throw new ArgumentNullException("selector"); - } - - T[] selectManyRecursive = source as T[] ?? source.ToArray(); - return !selectManyRecursive.Any() - ? selectManyRecursive - : selectManyRecursive.Concat( - selectManyRecursive - .SelectMany(i => selector(i).EmptyIfNull()) - .SelectManyRecursive(selector) - ); + return source ?? Enumerable.Empty(); } - public static IEnumerable RecursiveSelect(this IEnumerable source, - Func> childSelector) + public static IEnumerable RecursiveSelect(this IEnumerable source, Func> childSelector) { return RecursiveSelect(source, childSelector, element => element); } - public static IEnumerable RecursiveSelect(this IEnumerable source, - Func> - childSelector, - Func selector) + public static IEnumerable RecursiveSelect(this IEnumerable source, Func> childSelector, Func selector) { return RecursiveSelect(source, childSelector, (element, index, depth) => selector(element)); } - public static IEnumerable RecursiveSelect(this IEnumerable source, - Func> - childSelector, - Func selector) + public static IEnumerable RecursiveSelect(this IEnumerable source, Func> childSelector, Func selector) { return RecursiveSelect(source, childSelector, (element, index, depth) => selector(element, index)); } - public static IEnumerable RecursiveSelect(this IEnumerable source, - Func> - childSelector, - Func selector) + public static IEnumerable RecursiveSelect(this IEnumerable source, Func> childSelector, Func selector) { return RecursiveSelect(source, childSelector, selector, 0); } - private static IEnumerable RecursiveSelect(this IEnumerable source, - Func> - childSelector, - Func selector, - int depth) + public static IEnumerable SelectManyRecursive(this IEnumerable source, Func> selector) { - return source.SelectMany((element, index) => Enumerable.Repeat(selector(element, index, depth), 1) - .Concat( - RecursiveSelect( - childSelector(element) ?? - Enumerable.Empty(), - childSelector, selector, depth + 1))); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } + + T[] selectManyRecursive = source as T[] ?? source.ToArray(); + return !selectManyRecursive.Any() ? selectManyRecursive : selectManyRecursive.Concat(selectManyRecursive.SelectMany(i => selector(i).EmptyIfNull()).SelectManyRecursive(selector)); } - public static IEnumerable EmptyIfNull(this IEnumerable source) + private static IEnumerable RecursiveSelect(this IEnumerable source, Func> childSelector, Func selector, int depth) { - return source ?? Enumerable.Empty(); + return source.SelectMany((element, index) => Enumerable.Repeat(selector(element, index, depth), 1).Concat(RecursiveSelect(childSelector(element) ?? Enumerable.Empty(), childSelector, selector, depth + 1))); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/AggregateEnumerator.cs b/src/DynamicData/Aggregation/AggregateEnumerator.cs index 64c4abe9b..cbdd240eb 100644 --- a/src/DynamicData/Aggregation/AggregateEnumerator.cs +++ b/src/DynamicData/Aggregation/AggregateEnumerator.cs @@ -1,9 +1,10 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace DynamicData.Aggregation { @@ -25,6 +26,7 @@ public IEnumerator> GetEnumerator() case ListChangeReason.Add: yield return new AggregateItem(AggregateType.Add, change.Item.Current); break; + case ListChangeReason.AddRange: foreach (var item in change.Range) { @@ -32,13 +34,16 @@ public IEnumerator> GetEnumerator() } break; + case ListChangeReason.Replace: yield return new AggregateItem(AggregateType.Remove, change.Item.Previous.Value); yield return new AggregateItem(AggregateType.Add, change.Item.Current); break; + case ListChangeReason.Remove: yield return new AggregateItem(AggregateType.Remove, change.Item.Current); break; + case ListChangeReason.RemoveRange: case ListChangeReason.Clear: foreach (var item in change.Range) @@ -57,7 +62,9 @@ IEnumerator IEnumerable.GetEnumerator() } } + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Same name, different generics.")] internal class AggregateEnumerator : IAggregateChangeSet + where TKey : notnull { private readonly IChangeSet _source; @@ -75,13 +82,16 @@ public IEnumerator> GetEnumerator() case ChangeReason.Add: yield return new AggregateItem(AggregateType.Add, change.Current); break; + case ChangeReason.Update: yield return new AggregateItem(AggregateType.Remove, change.Previous.Value); yield return new AggregateItem(AggregateType.Add, change.Current); break; + case ChangeReason.Remove: yield return new AggregateItem(AggregateType.Remove, change.Current); break; + default: continue; } @@ -93,4 +103,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/AggregateItem.cs b/src/DynamicData/Aggregation/AggregateItem.cs index 713608197..341e2f50d 100644 --- a/src/DynamicData/Aggregation/AggregateItem.cs +++ b/src/DynamicData/Aggregation/AggregateItem.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,7 +8,7 @@ namespace DynamicData.Aggregation { /// - /// An object representing added and removed items in a continuous aggregation stream + /// An object representing added and removed items in a continuous aggregation stream. /// /// The type of the object. public readonly struct AggregateItem : IEquatable> @@ -34,33 +34,35 @@ public AggregateItem(AggregateType type, TObject item) /// public TObject Item { get; } - public override bool Equals(object obj) + public static bool operator ==(AggregateItem left, AggregateItem right) { - return obj is AggregateItem item && Equals(item); + return left.Equals(right); } - public bool Equals(AggregateItem other) + public static bool operator !=(AggregateItem left, AggregateItem right) { - return Type == other.Type && - EqualityComparer.Default.Equals(Item, other.Item); + return !(left == right); } - public override int GetHashCode() + /// + public override bool Equals(object? obj) { - var hashCode = -1719135621; - hashCode = hashCode * -1521134295 + Type.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Item); - return hashCode; + return obj is AggregateItem item && Equals(item); } - public static bool operator ==(AggregateItem left, AggregateItem right) + /// + public bool Equals(AggregateItem other) { - return left.Equals(right); + return Type == other.Type && EqualityComparer.Default.Equals(Item, other.Item); } - public static bool operator !=(AggregateItem left, AggregateItem right) + /// + public override int GetHashCode() { - return !(left == right); + var hashCode = -1719135621; + hashCode = (hashCode * -1521134295) + Type.GetHashCode(); + hashCode = (hashCode * -1521134295) + (Item is null ? 0 : EqualityComparer.Default.GetHashCode(Item)); + return hashCode; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/AggregateType.cs b/src/DynamicData/Aggregation/AggregateType.cs index c652028e6..fbb6684c7 100644 --- a/src/DynamicData/Aggregation/AggregateType.cs +++ b/src/DynamicData/Aggregation/AggregateType.cs @@ -1,22 +1,22 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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.Aggregation { /// - /// The type of aggregation + /// The type of aggregation. /// public enum AggregateType { /// - /// The add + /// The add. /// Add, /// - /// The remove + /// The remove. /// Remove } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/AggregationEx.cs b/src/DynamicData/Aggregation/AggregationEx.cs index 7d615cff9..987745555 100644 --- a/src/DynamicData/Aggregation/AggregationEx.cs +++ b/src/DynamicData/Aggregation/AggregationEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,53 +6,77 @@ using System.Linq; using System.Reactive; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.Aggregation { /// - /// Aggregation extensions + /// Aggregation extensions. /// public static class AggregationEx { /// - /// Transforms the changeset into an enumerable which is suitable for high performing aggregations + /// Transforms the change set into an enumerable which is suitable for high performing aggregations. /// /// The type of the object. /// The type of the key. /// The source. - /// - /// - public static IObservable> ForAggregation([NotNull] this IObservable> source) + /// The aggregated change set. + public static IObservable> ForAggregation(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Select(changeset => (IAggregateChangeSet)new AggregateEnumerator(changeset)); + return source.Select(changeSet => (IAggregateChangeSet)new AggregateEnumerator(changeSet)); } /// - /// Transforms the changeset into an enumerable which is suitable for high performing aggregations + /// Transforms the change set into an enumerable which is suitable for high performing aggregations. /// /// The type of the object. /// The source. - /// - /// - public static IObservable> ForAggregation([NotNull] this IObservable> source) + /// The aggregated change set. + public static IObservable> ForAggregation(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Select(changeset => (IAggregateChangeSet)new AggregateEnumerator(changeset)); + return source.Select(changeSet => (IAggregateChangeSet)new AggregateEnumerator(changeSet)); + } + + /// + /// Used to invalidate an aggregating stream. Used when there has been an inline change + /// i.e. a property changed or meta data has changed. + /// + /// The type of the item. + /// The source. + /// The invalidate. + /// An observable which emits the value. + public static IObservable InvalidateWhen(this IObservable source, IObservable invalidate) + { + return invalidate.StartWith(Unit.Default).Select(_ => source).Switch().DistinctUntilChanged(); + } + + /// + /// Used to invalidate an aggregating stream. Used when there has been an inline change. + /// + /// The type of the item. + /// The type of the trigger. + /// The source. + /// The invalidate. + /// An observable which emits the value. + public static IObservable InvalidateWhen(this IObservable source, IObservable invalidate) + { + return invalidate.StartWith(default(TTrigger)).Select(_ => source).Switch().DistinctUntilChanged(); } /// /// Applies an accumulator when items are added to and removed from specified stream, - /// starting with the initial seed + /// starting with the initial seed. /// /// The type of the object. /// The type of the result. @@ -61,19 +85,15 @@ public static IObservable> ForAggregation( /// The accessor. /// The add action. /// The remove action. - /// - internal static IObservable Accumlate([NotNull] this IObservable> source, - TResult seed, - [NotNull] Func accessor, - [NotNull] Func addAction, - [NotNull] Func removeAction) + /// An observable with the accumulated value. + internal static IObservable Accumulate(this IObservable> source, TResult seed, Func accessor, Func addAction, Func removeAction) { - return source.ForAggregation().Accumlate(seed, accessor, addAction, removeAction); + return source.ForAggregation().Accumulate(seed, accessor, addAction, removeAction); } /// /// Applies an accumulator when items are added to and removed from specified stream, - /// starting with the initial seed + /// starting with the initial seed. /// /// The type of the object. /// The type of the key. @@ -83,19 +103,16 @@ internal static IObservable Accumlate([NotNull] this /// The accessor. /// The add action. /// The remove action. - /// - internal static IObservable Accumlate([NotNull] this IObservable> source, - TResult seed, - [NotNull] Func accessor, - [NotNull] Func addAction, - [NotNull] Func removeAction) + /// An observable with the accumulated value. + internal static IObservable Accumulate(this IObservable> source, TResult seed, Func accessor, Func addAction, Func removeAction) + where TKey : notnull { - return source.ForAggregation().Accumlate(seed, accessor, addAction, removeAction); + return source.ForAggregation().Accumulate(seed, accessor, addAction, removeAction); } /// /// Applies an accumulator when items are added to and removed from specified stream, - /// starting with the initial seed + /// starting with the initial seed. /// /// The type of the object. /// The type of the result. @@ -104,73 +121,30 @@ internal static IObservable Accumlate([NotNull] /// The accessor. /// The add action. /// The remove action. - /// - internal static IObservable Accumlate([NotNull] this IObservable> source, - TResult seed, - [NotNull] Func accessor, - [NotNull] Func addAction, - [NotNull] Func removeAction) + /// An observable with the accumulated value. + internal static IObservable Accumulate(this IObservable> source, TResult seed, Func accessor, Func addAction, Func removeAction) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (accessor == null) + if (accessor is null) { throw new ArgumentNullException(nameof(accessor)); } - if (addAction == null) + if (addAction is null) { throw new ArgumentNullException(nameof(addAction)); } - if (removeAction == null) + if (removeAction is null) { throw new ArgumentNullException(nameof(removeAction)); } - return source.Scan(seed, (state, changes) => - { - return changes.Aggregate(state, (current, aggregateItem) => - aggregateItem.Type == AggregateType.Add - ? addAction(current, accessor(aggregateItem.Item)) - : removeAction(current, accessor(aggregateItem.Item)) - ); - }); - } - - /// - /// Used to invalidate an aggregating stream. Used when there has been an inline change - /// i.e. a property changed or meta data has changed - /// - /// - /// The source. - /// The invalidate. - /// - public static IObservable InvalidateWhen(this IObservable source, IObservable invalidate) - { - return invalidate.StartWith(Unit.Default) - .Select(_ => source) - .Switch() - .DistinctUntilChanged(); - } - - /// - /// Used to invalidate an aggregating stream. Used when there has been an inline change - /// - /// - /// The type of the trigger. - /// The source. - /// The invalidate. - /// - public static IObservable InvalidateWhen(this IObservable source, IObservable invalidate) - { - return invalidate.StartWith(default(TTrigger)) - .Select(_ => source) - .Switch() - .DistinctUntilChanged(); + return source.Scan(seed, (state, changes) => { return changes.Aggregate(state, (current, aggregateItem) => aggregateItem.Type == AggregateType.Add ? addAction(current, accessor(aggregateItem.Item)) : removeAction(current, accessor(aggregateItem.Item))); }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/Avg.cs b/src/DynamicData/Aggregation/Avg.cs index c856674b9..b57b5496c 100644 --- a/src/DynamicData/Aggregation/Avg.cs +++ b/src/DynamicData/Aggregation/Avg.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -13,6 +13,7 @@ public Avg(int count, TValue sum) } public int Count { get; } + public TValue Sum { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/AvgEx.cs b/src/DynamicData/Aggregation/AvgEx.cs index 080b5fbe0..14ea5ad0d 100644 --- a/src/DynamicData/Aggregation/AvgEx.cs +++ b/src/DynamicData/Aggregation/AvgEx.cs @@ -1,33 +1,31 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.Aggregation { /// - /// Average extensions + /// Average extensions. /// public static class AvgEx { - #region From IChangeSet - /// /// Continuous calculation of the average of the underlying data source. /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, int emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, int emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -37,13 +35,14 @@ public static IObservable Avg([NotNull] this IObservable< /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, int emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, int emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -53,14 +52,14 @@ public static IObservable Avg([NotNull] this IObservable< /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, long emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, long emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -70,13 +69,14 @@ public static IObservable Avg([NotNull] this IObservable< /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, long emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, long emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -86,14 +86,14 @@ public static IObservable Avg([NotNull] this IObservable< /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, double emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, double emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -103,13 +103,14 @@ public static IObservable Avg([NotNull] this IObservable< /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, double emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, double emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -119,14 +120,14 @@ public static IObservable Avg([NotNull] this IObservable< /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, decimal emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, decimal emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -136,13 +137,14 @@ public static IObservable Avg([NotNull] this IObservable /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, decimal emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, decimal emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -152,14 +154,14 @@ public static IObservable Avg([NotNull] this IObservable /// /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, float emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, float emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -169,33 +171,29 @@ public static IObservable Avg([NotNull] this IObservable /// The type of the object. /// The type of the key. - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, float emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, float emptyValue = 0) + where TKey : notnull { return source.ForAggregation().Avg(valueSelector, emptyValue); } - #endregion - - #region From IChangeSet - /// /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, int emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, int emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -203,14 +201,14 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, int emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, int emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -218,14 +216,14 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, long emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, long emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -233,14 +231,14 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, long emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, long emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -248,15 +246,14 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, double emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, double emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -264,14 +261,14 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, double emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, double emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -279,15 +276,14 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, decimal emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, decimal emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -295,14 +291,14 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, decimal emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, decimal emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -310,15 +306,14 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, float emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, float emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } @@ -326,53 +321,44 @@ public static IObservable Avg([NotNull] this IObservable /// /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, float emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, float emptyValue = 0) { return source.ForAggregation().Avg(valueSelector, emptyValue); } - #endregion - - #region From IAggregateChangeSet - /// /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, int emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, int emptyValue = 0) { - return source.AvgCalc(valueSelector, - emptyValue, - (current, item) => new Avg(current.Count + 1, current.Sum + item), - (current, item) => new Avg(current.Count - 1, current.Sum - item), - values => values.Sum / (double)values.Count); + return source.AvgCalc(valueSelector, emptyValue, (current, item) => new Avg(current.Count + 1, current.Sum + item), (current, item) => new Avg(current.Count - 1, current.Sum - item), values => values.Sum / (double)values.Count); } /// /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, int emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, int emptyValue = 0) { return source.Avg(t => valueSelector(t).GetValueOrDefault(), emptyValue); } @@ -380,31 +366,27 @@ public static IObservable Avg([NotNull] this IObservable /// Averages the specified value selector. /// - /// + /// The type to average. /// The source. /// The value selector. /// The empty value. - /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, long emptyValue = 0) + /// An observable of averages as a double value. + public static IObservable Avg(this IObservable> source, Func valueSelector, long emptyValue = 0) { - return source.AvgCalc(valueSelector, - emptyValue, - (current, item) => new Avg(current.Count + 1, current.Sum + item), - (current, item) => new Avg(current.Count - 1, current.Sum - item), - values => values.Sum / (double)values.Count); + return source.AvgCalc(valueSelector, emptyValue, (current, item) => new Avg(current.Count + 1, current.Sum + item), (current, item) => new Avg(current.Count - 1, current.Sum - item), values => values.Sum / (double)values.Count); } /// /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, long emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, long emptyValue = 0) { return source.Avg(t => valueSelector(t).GetValueOrDefault(), emptyValue); } @@ -412,34 +394,29 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, double emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, double emptyValue = 0) { - return source.AvgCalc(valueSelector, - emptyValue, - (current, item) => new Avg(current.Count + 1, current.Sum + item), - (current, item) => new Avg(current.Count - 1, current.Sum - item), - values => values.Sum / (double)values.Count); + return source.AvgCalc(valueSelector, emptyValue, (current, item) => new Avg(current.Count + 1, current.Sum + item), (current, item) => new Avg(current.Count - 1, current.Sum - item), values => values.Sum / (double)values.Count); } /// /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, double emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, double emptyValue = 0) { return source.Avg(t => valueSelector(t).GetValueOrDefault(), emptyValue); } @@ -447,32 +424,27 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, decimal emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, decimal emptyValue = 0) { - return source.AvgCalc(valueSelector, - emptyValue, - (current, item) => new Avg(current.Count + 1, current.Sum + item), - (current, item) => new Avg(current.Count - 1, current.Sum - item), - values => values.Sum / values.Count); + return source.AvgCalc(valueSelector, emptyValue, (current, item) => new Avg(current.Count + 1, current.Sum + item), (current, item) => new Avg(current.Count - 1, current.Sum - item), values => values.Sum / values.Count); } /// /// Averages the specified value selector. /// - /// + /// The type to average. /// The source. /// The value selector. /// The empty value. - /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, decimal emptyValue = 0) + /// An observable of decimals with the averaged values. + public static IObservable Avg(this IObservable> source, Func valueSelector, decimal emptyValue = 0) { return source.Avg(t => valueSelector(t).GetValueOrDefault(), emptyValue); } @@ -480,81 +452,61 @@ public static IObservable Avg([NotNull] this IObservable /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, - [NotNull] Func valueSelector, float emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, float emptyValue = 0) { - return source.AvgCalc(valueSelector, - emptyValue, - (current, item) => new Avg(current.Count + 1, current.Sum + item), - (current, item) => new Avg(current.Count - 1, current.Sum - item), - values => values.Sum / values.Count); + return source.AvgCalc(valueSelector, emptyValue, (current, item) => new Avg(current.Count + 1, current.Sum + item), (current, item) => new Avg(current.Count - 1, current.Sum - item), values => values.Sum / values.Count); } /// /// Continuous calculation of the average of the underlying data source. /// - /// - /// The source observable - /// The function which returns the value - /// The resulting average value when there is no data + /// The type to average. + /// The source observable. + /// The function which returns the value. + /// The resulting average value when there is no data. /// - /// An observable of averages + /// An observable of averages. /// - public static IObservable Avg([NotNull] this IObservable> source, [NotNull] Func valueSelector, float emptyValue = 0) + public static IObservable Avg(this IObservable> source, Func valueSelector, float emptyValue = 0) { return source.Avg(t => valueSelector(t).GetValueOrDefault(), emptyValue); } - #endregion - - private static IObservable AvgCalc(this IObservable> source, - Func valueSelector, - TResult fallbackValue, - [NotNull] Func, TValue, Avg> addAction, - [NotNull] Func, TValue, Avg> removeAction, - [NotNull] Func, TResult> resultAction) + private static IObservable AvgCalc(this IObservable> source, Func valueSelector, TResult fallbackValue, Func, TValue, Avg> addAction, Func, TValue, Avg> removeAction, Func, TResult> resultAction) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - if (addAction == null) + if (addAction is null) { throw new ArgumentNullException(nameof(addAction)); } - if (removeAction == null) + if (removeAction is null) { throw new ArgumentNullException(nameof(removeAction)); } - if (resultAction == null) + if (resultAction is null) { throw new ArgumentNullException(nameof(resultAction)); } - return source.Scan(default(Avg), (state, changes) => - { - return changes.Aggregate(state, (current, aggregateItem) => - aggregateItem.Type == AggregateType.Add - ? addAction(current, valueSelector(aggregateItem.Item)) - : removeAction(current, valueSelector(aggregateItem.Item)) - ); - }) - .Select(values => values.Count == 0 ? fallbackValue : resultAction(values)); + return source.Scan(default(Avg), (state, changes) => { return changes.Aggregate(state, (current, aggregateItem) => aggregateItem.Type == AggregateType.Add ? addAction(current, valueSelector(aggregateItem.Item)) : removeAction(current, valueSelector(aggregateItem.Item))); }).Select(values => values.Count == 0 ? fallbackValue : resultAction(values)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/CountEx.cs b/src/DynamicData/Aggregation/CountEx.cs index 2f4376573..b4e425ffe 100644 --- a/src/DynamicData/Aggregation/CountEx.cs +++ b/src/DynamicData/Aggregation/CountEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,117 +8,107 @@ namespace DynamicData.Aggregation { /// - /// Count extensions + /// Count extensions. /// public static class CountEx { /// - /// Counts the total number of items in the underlying data source + /// Counts the total number of items in the underlying data source. /// /// The type of the object. /// The type of the key. /// The source. - /// + /// An observable which emits the count. public static IObservable Count(this IObservable> source) + where TKey : notnull { return source.ForAggregation().Count(); } /// - /// Counts the total number of items in the underlying data source + /// Counts the total number of items in the underlying data source. /// /// The type of the object. /// The source. - /// + /// An observable which emits the count. public static IObservable Count(this IObservable> source) { return source.ForAggregation().Count(); } /// - /// Counts the total number of items in the underlying data source + /// Counts the total number of items in the underlying data source. /// /// The type of the object. /// The source. - /// + /// An observable which emits the count. public static IObservable Count(this IObservable> source) { - return source.Accumlate(0, t => 1, - (current, increment) => current + increment, - (current, increment) => current - increment); + return source.Accumulate(0, _ => 1, (current, increment) => current + increment, (current, increment) => current - increment); } /// - /// Counts the total number of items in the underlying data source + /// Counts the total number of items in the underlying data source. /// /// The type of the object. /// The source. - /// + /// An observable which emits the count. public static IObservable Count(this IObservable> source) + where TObject : notnull { return source.ForAggregation().Count(); } /// /// Counts the total number of items in the underlying data source - /// and return true if the number of items == 0 + /// and return true if the number of items == 0. /// /// The type of the object. /// The type of the key. /// The source. - /// + /// An observable which emits the count. public static IObservable IsEmpty(this IObservable> source) + where TKey : notnull { - return source.ForAggregation() - .Count() - .StartWith(0) - .Select(count => count == 0); + return source.ForAggregation().Count().StartWith(0).Select(count => count == 0); } /// /// Counts the total number of items in the underlying data source - /// and returns true if the number of items is greater than 0 + /// and return true if the number of items == 0. /// /// The type of the object. - /// The type of the key. /// The source. - /// - public static IObservable NotEmpty(this IObservable> source) + /// An observable which emits the count. + public static IObservable IsEmpty(this IObservable> source) { - return source.ForAggregation() - .Count() - .StartWith(0) - .Select(count => count > 0); + return source.ForAggregation().Count().StartWith(0).Select(count => count == 0); } /// /// Counts the total number of items in the underlying data source - /// and return true if the number of items == 0 + /// and returns true if the number of items is greater than 0. /// /// The type of the object. + /// The type of the key. /// The source. - /// - public static IObservable IsEmpty(this IObservable> source) + /// An observable which emits the count. + public static IObservable NotEmpty(this IObservable> source) + where TKey : notnull { - return source.ForAggregation() - .Count() - .StartWith(0) - .Select(count => count == 0); + return source.ForAggregation().Count().StartWith(0).Select(count => count > 0); } /// /// Counts the total number of items in the underlying data source - /// and returns true if the number of items is greater than 0 + /// and returns true if the number of items is greater than 0. /// /// The type of the object. /// The source. - /// + /// An observable which emits the count. public static IObservable NotEmpty(this IObservable> source) { - return source.ForAggregation() - .Count() - .StartWith(0) - .Select(count => count > 0); + return source.ForAggregation().Count().StartWith(0).Select(count => count > 0); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/IAggregateChangeSet.cs b/src/DynamicData/Aggregation/IAggregateChangeSet.cs index be41d0737..4d8cb1e16 100644 --- a/src/DynamicData/Aggregation/IAggregateChangeSet.cs +++ b/src/DynamicData/Aggregation/IAggregateChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,10 +7,10 @@ namespace DynamicData.Aggregation { /// - /// A changeset which has been shaped for rapid online aggregations + /// A change set which has been shaped for rapid online aggregations. /// - /// + /// The type of the item. public interface IAggregateChangeSet : IEnumerable> { } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/MaxEx.cs b/src/DynamicData/Aggregation/MaxEx.cs index e72fa48b3..174299619 100644 --- a/src/DynamicData/Aggregation/MaxEx.cs +++ b/src/DynamicData/Aggregation/MaxEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.Aggregation { @@ -15,202 +14,191 @@ namespace DynamicData.Aggregation /// public static class MaxEx { - #region Abstracted + private enum MaxOrMin + { + Max, + + Min + } /// - /// Continually calculates the maximum value from the underlying data source + /// Continually calculates the maximum value from the underlying data source. /// /// The type of the object. /// The type of the result. /// The source. /// The value selector. - /// The value to use when the underlying collection is empty + /// The value to use when the underlying collection is empty. /// - /// A distinct observable of the maximum item + /// A distinct observable of the maximum item. /// - public static IObservable Maximum([NotNull] this IObservable> source, - [NotNull] Func valueSelector, - TResult emptyValue = default(TResult)) + public static IObservable Maximum(this IObservable> source, Func valueSelector, TResult emptyValue = default) where TResult : struct, IComparable { return source.ToChangesAndCollection().Calculate(valueSelector, MaxOrMin.Max, emptyValue); } /// - /// Continually calculates the maximum value from the underlying data source + /// Continually calculates the maximum value from the underlying data source. /// /// The type of the object. /// The type of the key. /// The type of the result. /// The source. /// The value selector. - /// The value to use when the underlying collection is empty + /// The value to use when the underlying collection is empty. /// - /// A distinct observable of the maximum item + /// A distinct observable of the maximum item. /// - public static IObservable Maximum(this IObservable> source, Func valueSelector, TResult emptyValue = default(TResult)) + public static IObservable Maximum(this IObservable> source, Func valueSelector, TResult emptyValue = default) + where TKey : notnull where TResult : struct, IComparable { return source.ToChangesAndCollection().Calculate(valueSelector, MaxOrMin.Max, emptyValue); } /// - /// Continually calculates the minimum value from the underlying data source + /// Continually calculates the minimum value from the underlying data source. /// /// The type of the object. /// The type of the result. /// The source. /// The value selector. - /// The value to use when the underlying collection is empty - /// A distinct observable of the minimums item - public static IObservable Minimum(this IObservable> source, Func valueSelector, TResult emptyValue = default(TResult)) + /// The value to use when the underlying collection is empty. + /// A distinct observable of the minimums item. + public static IObservable Minimum(this IObservable> source, Func valueSelector, TResult emptyValue = default) where TResult : struct, IComparable { return source.ToChangesAndCollection().Calculate(valueSelector, MaxOrMin.Min, emptyValue); } /// - /// Continually calculates the minimum value from the underlying data source + /// Continually calculates the minimum value from the underlying data source. /// /// The type of the object. /// The type of the key. /// The type of the result. /// The source. /// The value selector. - /// The value to use when the underlying collection is empty + /// The value to use when the underlying collection is empty. /// - /// A distinct observable of the minimums item + /// A distinct observable of the minimums item. /// - public static IObservable Minimum(this IObservable> source, Func valueSelector, TResult emptyValue = default(TResult)) + public static IObservable Minimum(this IObservable> source, Func valueSelector, TResult emptyValue = default) + where TKey : notnull where TResult : struct, IComparable { return source.ToChangesAndCollection().Calculate(valueSelector, MaxOrMin.Min, emptyValue); } - #endregion - - private static IObservable Calculate([NotNull] this IObservable> source, - [NotNull] Func valueSelector, - MaxOrMin maxOrMin, - TResult emptyValue = default(TResult)) + private static IObservable Calculate(this IObservable> source, Func valueSelector, MaxOrMin maxOrMin, TResult emptyValue = default) where TResult : struct, IComparable { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Scan(default(TResult?), (state, latest) => - { - var current = state; - var requiresReset = false; - - foreach (var change in latest.Changes) - { - var value = valueSelector(change.Item); - if (!current.HasValue) + return source.Scan( + default(TResult?), + (state, latest) => { - current = value; - } + var current = state; + var requiresReset = false; - if (change.Type == AggregateType.Add) - { - int isMatched = maxOrMin == MaxOrMin.Max ? 1 : -1; - if (value.CompareTo(current.Value) == isMatched) + foreach (var change in latest.Changes) { - current = value; + var value = valueSelector(change.Item); + current ??= value; + + if (change.Type == AggregateType.Add) + { + int isMatched = maxOrMin == MaxOrMin.Max ? 1 : -1; + if (value.CompareTo(current.Value) == isMatched) + { + current = value; + } + } + else + { + // check whether the max / min has been removed. If so we need to look + // up the latest from the underlying collection + if (value.CompareTo(current.Value) != 0) + { + continue; + } + + requiresReset = true; + break; + } } - } - else - { - //check whether the max / min has been removed. If so we need to look - //up the latest from the underlying collection - if (value.CompareTo(current.Value) != 0) + + if (requiresReset) { - continue; + var collection = latest.Collection; + if (collection.Count == 0) + { + current = default; + } + else + { + current = maxOrMin == MaxOrMin.Max ? collection.Max(valueSelector) : collection.Min(valueSelector); + } } - requiresReset = true; - break; - } - } - - if (requiresReset) - { - var collecton = latest.Collection; - if (collecton.Count == 0) - { - current = default(TResult?); - } - else - { - current = maxOrMin == MaxOrMin.Max - ? collecton.Max(valueSelector) - : collecton.Min(valueSelector); - } - } - - return current; - }) - .Select(t => t ?? emptyValue) - .DistinctUntilChanged(); + return current; + }).Select(t => t ?? emptyValue).DistinctUntilChanged(); } - #region Helpers - - private static IObservable> ToChangesAndCollection([NotNull] this IObservable> source) + private static IObservable> ToChangesAndCollection(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Publish(shared => - { - var changes = shared.ForAggregation(); - var data = shared.ToCollection(); - return data.Zip(changes, (d, c) => new ChangesAndCollection(c, d)); - }); + return source.Publish( + shared => + { + var changes = shared.ForAggregation(); + var data = shared.ToCollection(); + return data.Zip(changes, (d, c) => new ChangesAndCollection(c, d)); + }); } - private static IObservable> ToChangesAndCollection([NotNull] this IObservable> source) + private static IObservable> ToChangesAndCollection(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Publish(shared => - { - var changes = shared.ForAggregation(); - var data = shared.ToCollection(); - return data.Zip(changes, (d, c) => new ChangesAndCollection(c, d)); - }); - } - - private enum MaxOrMin - { - Max, - Min + return source.Publish( + shared => + { + var changes = shared.ForAggregation(); + var data = shared.ToCollection(); + return data.Zip(changes, (d, c) => new ChangesAndCollection(c, d)); + }); } private class ChangesAndCollection { - public IAggregateChangeSet Changes { get; } - public IReadOnlyCollection Collection { get; } - public ChangesAndCollection(IAggregateChangeSet changes, IReadOnlyCollection collection) { Changes = changes; Collection = collection; } - } - #endregion + public IAggregateChangeSet Changes { get; } + + public IReadOnlyCollection Collection { get; } + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/StdDev.cs b/src/DynamicData/Aggregation/StdDev.cs index 0eca634ec..41312d4d4 100644 --- a/src/DynamicData/Aggregation/StdDev.cs +++ b/src/DynamicData/Aggregation/StdDev.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -19,4 +19,4 @@ public StdDev(int count, TValue sumOfItems, TValue sumOfSquares) public TValue SumOfSquares { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/StdDevEx.cs b/src/DynamicData/Aggregation/StdDevEx.cs index 300795f83..e3492eca9 100644 --- a/src/DynamicData/Aggregation/StdDevEx.cs +++ b/src/DynamicData/Aggregation/StdDevEx.cs @@ -1,324 +1,257 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.Aggregation { /// - /// Extensions for calculating standard deviation + /// Extensions for calculating standard deviation. /// public static class StdDevEx { - #region From IChangeSet - /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, [NotNull] Func valueSelector, int fallbackValue) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, int fallbackValue) { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, Func valueSelector, long fallbackValue) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, long fallbackValue) { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, Func valueSelector, double fallbackValue) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, double fallbackValue) { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// + /// An observable which emits the standard deviation value. public static IObservable StdDev(this IObservable> source, Func valueSelector, decimal fallbackValue) { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// + /// An observable which emits the standard deviation value. public static IObservable StdDev(this IObservable> source, Func valueSelector, float fallbackValue = 0) { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } - #endregion - - #region From IChangeSet - /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. /// The fallback value. - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, [NotNull] Func valueSelector, int fallbackValue) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, int fallbackValue) + where TKey : notnull { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. /// The fallback value. - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, Func valueSelector, long fallbackValue) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, long fallbackValue) + where TKey : notnull { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. /// The fallback value. - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, Func valueSelector, double fallbackValue) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, double fallbackValue) + where TKey : notnull { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. /// The fallback value. - /// - /// + /// An observable which emits the standard deviation value. public static IObservable StdDev(this IObservable> source, Func valueSelector, decimal fallbackValue) + where TKey : notnull { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. /// The fallback value. - /// - /// + /// An observable which emits the standard deviation value. public static IObservable StdDev(this IObservable> source, Func valueSelector, float fallbackValue = 0) + where TKey : notnull { return source.ForAggregation().StdDev(valueSelector, fallbackValue); } - #endregion - - #region From From IAggregateChangeSet - /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, - [NotNull] Func valueSelector, - int fallbackValue = 0) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, int fallbackValue = 0) { - return source.StdDevCalc(t => (long)valueSelector(t), - fallbackValue, - (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), - (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), - values => Math.Sqrt(values.SumOfSquares - (values.SumOfItems * values.SumOfItems) / values.Count) * (1.0d / (values.Count - 1))); + return source.StdDevCalc(t => (long)valueSelector(t), fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0d / (values.Count - 1))); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, - [NotNull] Func valueSelector, - long fallbackValue = 0) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, long fallbackValue = 0) { - return source.StdDevCalc(valueSelector, - fallbackValue, - (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), - (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), - values => Math.Sqrt(values.SumOfSquares - (values.SumOfItems * values.SumOfItems) / values.Count) * (1.0d / (values.Count - 1))); + return source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0d / (values.Count - 1))); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, - [NotNull] Func valueSelector, - decimal fallbackValue = 0M) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, decimal fallbackValue = 0M) { throw new NotImplementedException("For some reason there is a problem with decimal value inference"); - //return source.StdDevCalc(valueSelector, - // fallbackValue, - // (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), - // (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), - // values => Math.Sqrt((double)values.SumOfSquares - (double)(values.SumOfItems * values.SumOfItems) / values.Count) * (1.0d / (values.Count - 1))); + //// return source.StdDevCalc(valueSelector, + //// fallbackValue, + //// (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), + //// (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), + //// values => Math.Sqrt((double)values.SumOfSquares - (double)(values.SumOfItems * values.SumOfItems) / values.Count) * (1.0d / (values.Count - 1))); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, - [NotNull] Func valueSelector, - double fallbackValue = 0) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, double fallbackValue = 0) { - return source.StdDevCalc(valueSelector, - fallbackValue, - (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), - (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), - values => Math.Sqrt(values.SumOfSquares - (values.SumOfItems * values.SumOfItems) / values.Count) * (1.0d / (values.Count - 1))); + return source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0d / (values.Count - 1))); } /// - /// Continual computation of the standard deviation of the values in the underlying data source + /// Continual computation of the standard deviation of the values in the underlying data source. /// - /// + /// The type of the item. /// The source. /// The value selector. /// The fallback value. - /// - /// - /// - public static IObservable StdDev([NotNull] this IObservable> source, - [NotNull] Func valueSelector, - float fallbackValue = 0) + /// An observable which emits the standard deviation value. + public static IObservable StdDev(this IObservable> source, Func valueSelector, float fallbackValue = 0) { - return source.StdDevCalc(valueSelector, - fallbackValue, - (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), - (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), - values => Math.Sqrt(values.SumOfSquares - (values.SumOfItems * values.SumOfItems) / values.Count) * (1.0d / (values.Count - 1))); + return source.StdDevCalc(valueSelector, fallbackValue, (current, item) => new StdDev(current.Count + 1, current.SumOfItems + item, current.SumOfSquares + (item * item)), (current, item) => new StdDev(current.Count - 1, current.SumOfItems - item, current.SumOfSquares - (item * item)), values => Math.Sqrt(values.SumOfSquares - ((values.SumOfItems * values.SumOfItems) / values.Count)) * (1.0d / (values.Count - 1))); } - private static IObservable StdDevCalc(this IObservable> source, - Func valueSelector, - TResult fallbackValue, - [NotNull] Func, TValue, StdDev> addAction, - [NotNull] Func, TValue, StdDev> removeAction, - [NotNull] Func, TResult> resultAction) + private static IObservable StdDevCalc(this IObservable> source, Func valueSelector, TResult fallbackValue, Func, TValue, StdDev> addAction, Func, TValue, StdDev> removeAction, Func, TResult> resultAction) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - if (addAction == null) + if (addAction is null) { throw new ArgumentNullException(nameof(addAction)); } - if (removeAction == null) + if (removeAction is null) { throw new ArgumentNullException(nameof(removeAction)); } - if (resultAction == null) + if (resultAction is null) { throw new ArgumentNullException(nameof(resultAction)); } - return source.Scan(default(StdDev), (state, changes) => - { - return changes.Aggregate(state, (current, aggregateItem) => - aggregateItem.Type == AggregateType.Add - ? addAction(current, valueSelector(aggregateItem.Item)) - : removeAction(current, valueSelector(aggregateItem.Item)) - ); - }) - .Select(values => values.Count < 2 ? fallbackValue : resultAction(values)); + return source.Scan(default(StdDev), (state, changes) => { return changes.Aggregate(state, (current, aggregateItem) => aggregateItem.Type == AggregateType.Add ? addAction(current, valueSelector(aggregateItem.Item)) : removeAction(current, valueSelector(aggregateItem.Item))); }).Select(values => values.Count < 2 ? fallbackValue : resultAction(values)); } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/Aggregation/SumEx.cs b/src/DynamicData/Aggregation/SumEx.cs index b21dbcb50..00ab683d5 100644 --- a/src/DynamicData/Aggregation/SumEx.cs +++ b/src/DynamicData/Aggregation/SumEx.cs @@ -1,533 +1,486 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Aggregation { /// - /// Aggregation extensions + /// Aggregation extensions. /// public static class SumEx { - #region From IChangeSet - /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// /// The type of the object. /// The type of the key. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) + where TKey : notnull { return source.ForAggregation().Sum(valueSelector); } - #endregion - - #region From IChangeSet - /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { return source.ForAggregation().Sum(valueSelector); } - #endregion - - #region From IAggregateChangeSet - /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0, - valueSelector, - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - return source.Accumlate(0, - t => valueSelector(t).GetValueOrDefault(), - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0, t => valueSelector(t).GetValueOrDefault(), (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0, - valueSelector, - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0L, - t => valueSelector(t).ValueOr(0), - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0L, t => valueSelector(t).ValueOr(0), (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0, - valueSelector, - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0D, - t => valueSelector(t).ValueOr(0), - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0D, t => valueSelector(t).ValueOr(0), (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0, - valueSelector, - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0M, - t => valueSelector(t).ValueOr(0), - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0M, t => valueSelector(t).ValueOr(0), (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, - [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0, - valueSelector, - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0, valueSelector, (current, value) => current + value, (current, value) => current - value); } /// - /// Continual computes the sum of values matching the value selector + /// Continual computes the sum of values matching the value selector. /// - /// + /// The type of the item. /// The source. /// The value selector. - /// - public static IObservable Sum([NotNull] this IObservable> source, [NotNull] Func valueSelector) + /// An observable which emits the summed value. + public static IObservable Sum(this IObservable> source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.Accumlate(0F, - t => valueSelector(t).ValueOr(0), - (current, value) => current + value, - (current, value) => current - value); + return source.Accumulate(0F, t => valueSelector(t).ValueOr(0), (current, value) => current + value, (current, value) => current - value); } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/Alias/ObservableCacheAlias.cs b/src/DynamicData/Alias/ObservableCacheAlias.cs index d8db4913a..44233fa84 100644 --- a/src/DynamicData/Alias/ObservableCacheAlias.cs +++ b/src/DynamicData/Alias/ObservableCacheAlias.cs @@ -1,155 +1,49 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; - using System.Reactive; -using DynamicData.Annotations; +using System.Reactive; + using DynamicData.Kernel; namespace DynamicData.Alias { /// - /// Observable cache alias names + /// Observable cache alias names. /// public static class ObservableCacheAlias { - #region Filter -> Where - - /// - /// Filters the specified source. - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The filter. - /// - public static IObservable> Where(this IObservable> source, Func filter) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source.Filter(filter); - } - - /// - /// Creates a filtered stream which can be dynamically filtered - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// Observable to change the underlying predicate. - /// - public static IObservable> Where([NotNull] this IObservable> source, - [NotNull] IObservable> predicateChanged) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (predicateChanged == null) - { - throw new ArgumentNullException(nameof(predicateChanged)); - } - - return source.Filter(predicateChanged); - } - - /// - /// Creates a filtered stream which can be dynamically filtered - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// Observable to re-evaluate whether the filter still matches items. Use when filtering on mutable values - /// - /// - /// - public static IObservable> Where([NotNull] this IObservable> source, - [NotNull] IObservable reapplyFilter) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (reapplyFilter == null) - { - throw new ArgumentNullException(nameof(reapplyFilter)); - } - - return source.Filter(reapplyFilter); - } - - /// - /// Creates a filtered stream which can be dynamically filtered - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// Observable to re-evaluate whether the filter still matches items. Use when filtering on mutable values - /// Observable to change the underlying predicate. - /// - public static IObservable> Where([NotNull] this IObservable> source, - [NotNull] IObservable> predicateChanged, - [NotNull] IObservable reapplyFilter) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (predicateChanged == null) - { - throw new ArgumentNullException(nameof(predicateChanged)); - } - - if (reapplyFilter == null) - { - throw new ArgumentNullException(nameof(reapplyFilter)); - } - - return source.Filter(predicateChanged, reapplyFilter); - } - - #endregion - - #region Transform -> Select - /// - /// Projects each update item to a new form using the specified transform function + /// Projects each update item to a new form using the specified transform function. /// /// The type of the destination. /// The type of the source. /// The type of the key. /// The source. /// The transform factory. - /// Invoke to force a new transform for all items# + /// Invoke to force a new transform for all items.# /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> Select(this IObservable> source, - Func transformFactory, - IObservable forceTransform) + /// transformFactory. + public static IObservable> Select(this IObservable> source, Func transformFactory, IObservable forceTransform) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (transformFactory is null) { throw new ArgumentNullException(nameof(transformFactory)); } - if (forceTransform == null) + if (forceTransform is null) { throw new ArgumentNullException(nameof(forceTransform)); } @@ -158,74 +52,71 @@ public static IObservable> Select - /// Projects each update item to a new form using the specified transform function + /// Projects each update item to a new form using the specified transform function. /// /// The type of the destination. /// The type of the source. /// The type of the key. /// The source. /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects + /// Invoke to force a new transform for items matching the selected objects. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> Select(this IObservable> source, - Func transformFactory, - IObservable> forceTransform = null) + /// transformFactory. + public static IObservable> Select(this IObservable> source, Func transformFactory, IObservable>? forceTransform = null) + where TKey : notnull { return source.Transform(transformFactory, forceTransform); } /// - /// Projects each update item to a new form using the specified transform function + /// Projects each update item to a new form using the specified transform function. /// /// The type of the destination. /// The type of the source. /// The type of the key. /// The source. /// The transform factory. - /// Invoke to force a new transform for all items + /// Invoke to force a new transform for all items. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> Select(this IObservable> source, - Func transformFactory, - IObservable forceTransform) + /// transformFactory. + public static IObservable> Select(this IObservable> source, Func transformFactory, IObservable forceTransform) + where TKey : notnull { return source.Transform(transformFactory, forceTransform); } /// - /// Projects each update item to a new form using the specified transform function + /// Projects each update item to a new form using the specified transform function. /// /// The type of the destination. /// The type of the source. /// The type of the key. /// The source. /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects + /// Invoke to force a new transform for items matching the selected objects. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> Select(this IObservable> source, - Func transformFactory, - IObservable> forceTransform = null) + /// transformFactory. + public static IObservable> Select(this IObservable> source, Func transformFactory, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (transformFactory is null) { throw new ArgumentNullException(nameof(transformFactory)); } @@ -233,35 +124,6 @@ public static IObservable> Select - /// Transforms the object to a fully recursive tree, create a hiearchy based on the pivot function - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The pivot on. - /// - public static IObservable, TKey>> SelectTree([NotNull] this IObservable> source, - [NotNull] Func pivotOn) - where TObject : class - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (pivotOn == null) - { - throw new ArgumentNullException(nameof(pivotOn)); - } - - return source.TransformToTree(pivotOn); - } - - #endregion - - #region Transform many -> SelectMany - /// /// Equivalent to a select many transform. To work, the key must individually identify each child. /// @@ -270,20 +132,16 @@ public static IObservable, TKey>> SelectTreeThe type of the source. /// The type of the source key. /// The source. - /// The manyselector. - /// The key selector which must be unique across all - /// - public static IObservable> SelectMany( - this IObservable> source, - Func> manyselector, Func keySelector) + /// The selector for selecting the enumerable. + /// The key selector which must be unique across all. + /// An observable which emits the change set. + public static IObservable> SelectMany(this IObservable> source, Func> manySelector, Func keySelector) + where TDestinationKey : notnull + where TSourceKey : notnull { - return source.TransformMany(manyselector, keySelector); + return source.TransformMany(manySelector, keySelector); } - #endregion - - #region Transform safe -> SelectSafe - /// /// Projects each update item to a new form using the specified transform function, /// providing an error handling action to safely handle transform errors without killing the stream. @@ -296,34 +154,32 @@ public static IObservable> SelectMany< /// Provides the option to safely handle errors without killing the stream. /// If not specified the stream will terminate as per rx convention. /// - /// Invoke to force a new transform for items matching the selected objects + /// Invoke to force a new transform for items matching the selected objects. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> SelectSafe(this IObservable> source, - Func transformFactory, - Action> errorHandler, - IObservable forceTransform) + /// transformFactory. + public static IObservable> SelectSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable forceTransform) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (transformFactory is null) { throw new ArgumentNullException(nameof(transformFactory)); } - if (errorHandler == null) + if (errorHandler is null) { throw new ArgumentNullException(nameof(errorHandler)); } - if (forceTransform == null) + if (forceTransform is null) { throw new ArgumentNullException(nameof(forceTransform)); } @@ -343,29 +199,27 @@ public static IObservable> SelectSafeProvides the option to safely handle errors without killing the stream. /// If not specified the stream will terminate as per rx convention. /// - /// Invoke to force a new transform for items matching the selected objects + /// Invoke to force a new transform for items matching the selected objects. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> SelectSafe(this IObservable> source, - Func transformFactory, - Action> errorHandler, - IObservable> forceTransform = null) + /// transformFactory. + public static IObservable> SelectSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (transformFactory is null) { throw new ArgumentNullException(nameof(transformFactory)); } - if (errorHandler == null) + if (errorHandler is null) { throw new ArgumentNullException(nameof(errorHandler)); } @@ -385,29 +239,27 @@ public static IObservable> SelectSafeProvides the option to safely handle errors without killing the stream. /// If not specified the stream will terminate as per rx convention. /// - /// Invoke to force a new transform for items matching the selected objects + /// Invoke to force a new transform for items matching the selected objects. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> SelectSafe(this IObservable> source, - Func transformFactory, - Action> errorHandler, - IObservable> forceTransform = null) + /// transformFactory. + public static IObservable> SelectSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (transformFactory is null) { throw new ArgumentNullException(nameof(transformFactory)); } - if (errorHandler == null) + if (errorHandler is null) { throw new ArgumentNullException(nameof(errorHandler)); } @@ -427,21 +279,139 @@ public static IObservable> SelectSafeProvides the option to safely handle errors without killing the stream. /// If not specified the stream will terminate as per rx convention. /// - /// Invoke to force a new transform for all items + /// Invoke to force a new transform for all items. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> SelectSafe(this IObservable> source, - Func transformFactory, - Action> errorHandler, - IObservable forceTransform) + /// transformFactory. + public static IObservable> SelectSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable forceTransform) + where TKey : notnull { return source.TransformSafe(transformFactory, errorHandler, forceTransform); } - #endregion + /// + /// Transforms the object to a fully recursive tree, create a hierarchy based on the pivot function. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The pivot on. + /// An observable which emits the change set. + public static IObservable, TKey>> SelectTree(this IObservable> source, Func pivotOn) + where TKey : notnull + where TObject : class + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (pivotOn is null) + { + throw new ArgumentNullException(nameof(pivotOn)); + } + + return source.TransformToTree(pivotOn); + } + + /// + /// Filters the specified source. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The filter. + /// An observable which emits the change set. + public static IObservable> Where(this IObservable> source, Func filter) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return source.Filter(filter); + } + + /// + /// Creates a filtered stream which can be dynamically filtered. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// Observable to change the underlying predicate. + /// An observable which emits the change set. + public static IObservable> Where(this IObservable> source, IObservable> predicateChanged) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicateChanged is null) + { + throw new ArgumentNullException(nameof(predicateChanged)); + } + + return source.Filter(predicateChanged); + } + + /// + /// Creates a filtered stream which can be dynamically filtered. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// Observable to re-evaluate whether the filter still matches items. Use when filtering on mutable values. + /// An observable which emits the change set. + public static IObservable> Where(this IObservable> source, IObservable reapplyFilter) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (reapplyFilter is null) + { + throw new ArgumentNullException(nameof(reapplyFilter)); + } + + return source.Filter(reapplyFilter); + } + + /// + /// Creates a filtered stream which can be dynamically filtered. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// Observable to change the underlying predicate. + /// Observable to re-evaluate whether the filter still matches items. Use when filtering on mutable values. + /// An observable which emits the change set. + public static IObservable> Where(this IObservable> source, IObservable> predicateChanged, IObservable reapplyFilter) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (predicateChanged is null) + { + throw new ArgumentNullException(nameof(predicateChanged)); + } + + if (reapplyFilter is null) + { + throw new ArgumentNullException(nameof(reapplyFilter)); + } + + return source.Filter(predicateChanged, reapplyFilter); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Alias/ObservableListAlias.cs b/src/DynamicData/Alias/ObservableListAlias.cs index f74c342e0..e2b8ba06c 100644 --- a/src/DynamicData/Alias/ObservableListAlias.cs +++ b/src/DynamicData/Alias/ObservableListAlias.cs @@ -1,129 +1,115 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; -using DynamicData.Annotations; namespace DynamicData.Alias { /// - /// Observable cache alias names + /// Observable cache alias names. /// public static class ObservableListAlias { - #region Filter -> Where - /// - /// Filters the source using the specified valueSelector + /// Projects each update item to a new form using the specified transform function. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// The valueSelector. - /// - /// source - public static IObservable> Where(this IObservable> source, Func predicate) + /// The transform factory. + /// An observable which emits the change set. + /// + /// source + /// or + /// valueSelector. + /// + public static IObservable> Select(this IObservable> source, Func transformFactory) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (predicate == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(predicate)); + throw new ArgumentNullException(nameof(transformFactory)); } - return source.Filter(predicate); + return source.Transform(transformFactory); } /// - /// Filters source using the specified filter observable predicate. + /// Equivalent to a select many transform. To work, the key must individually identify each child. + /// **** Assumes each child can only have one parent - support for children with multiple parents is a work in progresses. /// - /// + /// The type of the destination. + /// The type of the source. /// The source. - /// - /// - /// source - /// or - /// filterController - public static IObservable> Where([NotNull] this IObservable> source, [NotNull] IObservable> predicate) + /// The selector for the enumerable. + /// An observable which emits the change set. + public static IObservable> SelectMany(this IObservable> source, Func> manySelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (predicate == null) + if (manySelector is null) { - throw new ArgumentNullException(nameof(predicate)); + throw new ArgumentNullException(nameof(manySelector)); } - return source.Filter(predicate); + return source.TransformMany(manySelector); } - #endregion - - #region Transform -> Select - /// - /// Projects each update item to a new form using the specified transform function + /// Filters the source using the specified valueSelector. /// - /// The type of the source. - /// The type of the destination. + /// The type of the item. /// The source. - /// The transform factory. - /// - /// - /// source - /// or - /// valueSelector - /// - public static IObservable> Select(this IObservable> source, Func transformFactory) + /// The valueSelector. + /// An observable which emits the change set. + /// source. + public static IObservable> Where(this IObservable> source, Func predicate) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (predicate is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(predicate)); } - return source.Transform(transformFactory); + return source.Filter(predicate); } /// - /// Equivalent to a select many transform. To work, the key must individually identify each child. - /// **** Assumes each child can only have one parent - support for children with multiple parents is a work in progresses + /// Filters source using the specified filter observable predicate. /// - /// The type of the destination. - /// The type of the source. + /// The type of the item. /// The source. - /// The manyselector. - /// - /// - /// source + /// The predict for deciding on items to filter. + /// An observable which emits the change set. + /// source /// or - /// manyselector - /// - public static IObservable> SelectMany([NotNull] this IObservable> source, [NotNull] Func> manyselector) + /// filterController. + public static IObservable> Where(this IObservable> source, IObservable> predicate) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (manyselector == null) + if (predicate is null) { - throw new ArgumentNullException(nameof(manyselector)); + throw new ArgumentNullException(nameof(predicate)); } - return source.TransformMany(manyselector); + return source.Filter(predicate); } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/Attributes.cs b/src/DynamicData/Attributes.cs index 5d2d3ef98..08ae2edc9 100644 --- a/src/DynamicData/Attributes.cs +++ b/src/DynamicData/Attributes.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,4 +6,4 @@ [assembly: InternalsVisibleTo("DynamicData.Tests")] [assembly: InternalsVisibleTo("DynamicData.ReactiveUI")] -[assembly: InternalsVisibleTo("DynamicData.Profile")] +[assembly: InternalsVisibleTo("DynamicData.Profile")] \ No newline at end of file diff --git a/src/DynamicData/Binding/AbstractNotifyPropertyChanged.cs b/src/DynamicData/Binding/AbstractNotifyPropertyChanged.cs index 62b1b28bd..57ca6fb4a 100644 --- a/src/DynamicData/Binding/AbstractNotifyPropertyChanged.cs +++ b/src/DynamicData/Binding/AbstractNotifyPropertyChanged.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,54 +8,65 @@ using System.Diagnostics.CodeAnalysis; using System.Reactive.Disposables; using System.Runtime.CompilerServices; -using DynamicData.Annotations; namespace DynamicData.Binding { /// - /// Base class for implementing notify property changes + /// Base class for implementing notify property changes. /// public abstract class AbstractNotifyPropertyChanged : INotifyPropertyChanged { /// /// Occurs when a property value has changed. /// - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; /// - /// Invokes on property changed + /// Suspends notifications. When disposed, a reset notification is fired. + /// + /// If the property changed event should be invoked when disposed. + /// A disposable to indicate to stop suspending the notifications. + [Obsolete("This never worked properly in the first place")] + [SuppressMessage("Design", "CA1822: Make static", Justification = "Backwards compatibility")] + public IDisposable SuspendNotifications(bool invokePropertyChangeEventWhenDisposed = true) + { + // Removed code because it adds weight to the object + return Disposable.Empty; + } + + /// + /// Invokes on property changed. /// /// Name of the property. - [NotifyPropertyChangedInvocator] - protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } /// - /// If the value has changed, sets referenced backing field and raise notify property changed + /// If the value has changed, sets referenced backing field and raise notify property changed. /// - /// + /// The type to set and raise. /// The backing field. /// The new value. /// Name of the property. - protected virtual void SetAndRaise(ref T backingField, T newValue, [CallerMemberName] string propertyName = null) + protected virtual void SetAndRaise(ref T backingField, T newValue, [CallerMemberName] string? propertyName = null) { // ReSharper disable once ExplicitCallerInfoArgument SetAndRaise(ref backingField, newValue, EqualityComparer.Default, propertyName); } /// - /// If the value has changed, sets referenced backing field and raise notify property changed + /// If the value has changed, sets referenced backing field and raise notify property changed. /// - /// + /// The type of the item. /// The backing field. /// The new value. /// The comparer. /// Name of the property. - protected virtual void SetAndRaise(ref T backingField, T newValue, IEqualityComparer comparer, [CallerMemberName] string propertyName = null) + protected virtual void SetAndRaise(ref T backingField, T newValue, IEqualityComparer? comparer, [CallerMemberName] string? propertyName = null) { - comparer = comparer ?? EqualityComparer.Default; + comparer ??= EqualityComparer.Default; if (comparer.Equals(backingField, newValue)) { return; @@ -64,18 +75,5 @@ protected virtual void SetAndRaise(ref T backingField, T newValue, IEqualityC backingField = newValue; OnPropertyChanged(propertyName); } - - /// - /// Suspends notifications. When disposed, a reset notification is fired - /// - /// A disposable to indicate to stop suspending the notifications. - [Obsolete("This never worked properly in the first place")] - [SuppressMessage("Design", "CA1822: Make static", Justification = "Backwards compatibility")] - public IDisposable SuspendNotifications(bool invokePropertyChangeEventWhenDisposed = true) - { - - //Removed code because it adds weight to the object - return Disposable.Empty; - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/BindingListAdaptor.cs b/src/DynamicData/Binding/BindingListAdaptor.cs index 2e3f2b35c..eb000a74c 100644 --- a/src/DynamicData/Binding/BindingListAdaptor.cs +++ b/src/DynamicData/Binding/BindingListAdaptor.cs @@ -1,27 +1,34 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. #if SUPPORTS_BINDINGLIST - using System; using System.ComponentModel; -using DynamicData.Annotations; +using System.Diagnostics.CodeAnalysis; + using DynamicData.Cache.Internal; namespace DynamicData.Binding { /// - /// Adaptor to reflect a change set into a binding list + /// Adaptor to reflect a change set into a binding list. /// + /// The type of items. public class BindingListAdaptor : IChangeSetAdaptor { private readonly BindingList _list; + private readonly int _refreshThreshold; + private bool _loaded; - /// - public BindingListAdaptor([NotNull] BindingList list, int refreshThreshold = 25) + /// + /// Initializes a new instance of the class. + /// + /// The list of items to add to the adapter. + /// The threshold before a reset is issued. + public BindingListAdaptor(BindingList list, int refreshThreshold = 25) { _list = list ?? throw new ArgumentNullException(nameof(list)); _refreshThreshold = refreshThreshold; @@ -30,7 +37,7 @@ public BindingListAdaptor([NotNull] BindingList list, int refreshThreshold = /// public void Adapt(IChangeSet changes) { - if (changes == null) + if (changes is null) { throw new ArgumentNullException(nameof(changes)); } @@ -51,18 +58,28 @@ public void Adapt(IChangeSet changes) } /// - /// Adaptor to reflect a change set into a binding list + /// Adaptor to reflect a change set into a binding list. /// + /// The type of the object. + /// The type of the key. + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Same class name, different generics")] public class BindingListAdaptor : IChangeSetAdaptor + where TKey : notnull { + private readonly Cache _cache = new(); + private readonly BindingList _list; + private readonly int _refreshThreshold; - private bool _loaded; - private readonly Cache _cache = new Cache(); + private bool _loaded; - /// - public BindingListAdaptor([NotNull] BindingList list, int refreshThreshold = 25) + /// + /// Initializes a new instance of the class. + /// + /// The list of items to adapt. + /// The threshold before the refresh is triggered. + public BindingListAdaptor(BindingList list, int refreshThreshold = 25) { _list = list ?? throw new ArgumentNullException(nameof(list)); _refreshThreshold = refreshThreshold; @@ -101,8 +118,15 @@ private static void DoUpdate(IChangeSet changes, BindingList: IDisposable + internal sealed class BindingListEventsSuspender : IDisposable { private readonly IDisposable _cleanUp; @@ -18,11 +17,12 @@ public BindingListEventsSuspender(BindingList list) { list.RaiseListChangedEvents = false; - _cleanUp = Disposable.Create(() => - { - list.RaiseListChangedEvents = true; - list.ResetBindings(); - }); + _cleanUp = Disposable.Create( + () => + { + list.RaiseListChangedEvents = true; + list.ResetBindings(); + }); } public void Dispose() @@ -31,4 +31,5 @@ public void Dispose() } } } + #endif \ No newline at end of file diff --git a/src/DynamicData/Binding/BindingListEx.cs b/src/DynamicData/Binding/BindingListEx.cs index 166549bca..0543f3ce6 100644 --- a/src/DynamicData/Binding/BindingListEx.cs +++ b/src/DynamicData/Binding/BindingListEx.cs @@ -1,32 +1,41 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Reactive; using System.Reactive.Linq; namespace DynamicData.Binding { /// - /// Extensions to convert an binding list into a dynamic stream + /// Extensions to convert an binding list into a dynamic stream. /// public static class BindingListEx { + /// + /// Observes list changed args. + /// + /// The source list. + /// An observable which emits event pattern changed event args. + public static IObservable> ObserveCollectionChanges(this IBindingList source) + { + return Observable.FromEventPattern(h => source.ListChanged += h, h => source.ListChanged -= h); + } + /// /// Convert a binding list into an observable change set. /// Change set observes list change events. /// /// The type of the object. /// The source. - /// - /// source + /// An observable which emits change set values. + /// source. public static IObservable> ToObservableChangeSet(this BindingList source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -42,18 +51,19 @@ public static IObservable> ToObservableChangeSet(this BindingLi /// The type of the key. /// The source. /// The key selector. - /// + /// An observable which emits change set values. /// source /// or - /// keySelector + /// keySelector. public static IObservable> ToObservableChangeSet(this BindingList source, Func keySelector) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } @@ -65,77 +75,66 @@ public static IObservable> ToObservableChangeSet + /// The collection type. /// The type of the object. - /// /// The source. - /// - /// source + /// An observable which emits change set values. + /// source. public static IObservable> ToObservableChangeSet(this TCollection source) where TCollection : IBindingList, IEnumerable { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return Observable.Create>(observer => - { - var data = new ChangeAwareList(source); - - if (data.Count > 0) - { - observer.OnNext(data.CaptureChanges()); - } - - return source.ObserveCollectionChanges() - .Scan(data, (list, args) => + return Observable.Create>( + observer => { - var changes = args.EventArgs; + var data = new ChangeAwareList(source); - switch (changes.ListChangedType) + if (data.Count > 0) { - case ListChangedType.ItemAdded: - { - list.Add((T)source[changes.NewIndex]); - break; - } - - case ListChangedType.ItemDeleted: - { - list.RemoveAt(changes.NewIndex); - break; - } - - case ListChangedType.ItemChanged: - { - list[changes.NewIndex] = (T)source[changes.NewIndex]; - break; - } - - case ListChangedType.Reset: - { - list.Clear(); - list.AddRange(source); - break; - } + observer.OnNext(data.CaptureChanges()); } - return list; - }) - .Select(list => list.CaptureChanges()) - .SubscribeSafe(observer); - }); - } - - /// - /// Observes list changed args - /// - public static IObservable> ObserveCollectionChanges(this IBindingList source) - { - return Observable - .FromEventPattern( - h => source.ListChanged += h, - h => source.ListChanged -= h); + return source.ObserveCollectionChanges().Scan( + data, + (list, args) => + { + var changes = args.EventArgs; + + switch (changes.ListChangedType) + { + case ListChangedType.ItemAdded when source[changes.NewIndex] is T newItem: + { + list.Add(newItem); + break; + } + + case ListChangedType.ItemDeleted: + { + list.RemoveAt(changes.NewIndex); + break; + } + + case ListChangedType.ItemChanged when source[changes.NewIndex] is T newItem: + { + list[changes.NewIndex] = newItem; + break; + } + + case ListChangedType.Reset: + { + list.Clear(); + list.AddRange(source); + break; + } + } + + return list; + }).Select(list => list.CaptureChanges()).SubscribeSafe(observer); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/ExpressionBuilder.cs b/src/DynamicData/Binding/ExpressionBuilder.cs index 0d744e32e..f5b73ae95 100644 --- a/src/DynamicData/Binding/ExpressionBuilder.cs +++ b/src/DynamicData/Binding/ExpressionBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -13,105 +13,106 @@ namespace DynamicData.Binding { - internal static class ExpressionBuilder { - internal static string ToCacheKey(this Expression> expression) - where TObject:INotifyPropertyChanged - { - var members = expression.GetMembers(); - - IEnumerable GetNames() - { - yield return typeof(TObject).FullName; - foreach (var member in members.Reverse()) - { - yield return member.Member.Name; - } - } - - return string.Join(".",GetNames()); - } - public static IEnumerable GetMembers(this Expression> source) { var memberExpression = source.Body as MemberExpression; - while (memberExpression != null) + while (memberExpression is not null) { yield return memberExpression; memberExpression = memberExpression.Expression as MemberExpression; } } - internal static IEnumerable GetMemberChain(this Expression> expression) + internal static Func> CreatePropertyChangedFactory(this MemberExpression source) { - var memberExpression = expression.Body as MemberExpression; - while (memberExpression != null) - { - if (memberExpression.Expression.NodeType != ExpressionType.Parameter) - { - var parent = memberExpression.Expression; - yield return memberExpression.Update(Expression.Parameter(parent.Type)); - } - else - { - yield return memberExpression; - } + var property = source.GetProperty(); - memberExpression = memberExpression.Expression as MemberExpression; + if (property.DeclaringType is null) + { + throw new ArgumentException("The property does not have a valid declaring type.", nameof(source)); } + + var notifyPropertyChanged = typeof(INotifyPropertyChanged).GetTypeInfo().IsAssignableFrom(property.DeclaringType.GetTypeInfo()); + + return t => + { + if (t is null) + { + return Observable.Never; + } + + if (!notifyPropertyChanged) + { + return Observable.Return(Unit.Default); + } + + return Observable.FromEventPattern(handler => ((INotifyPropertyChanged)t).PropertyChanged += handler, handler => ((INotifyPropertyChanged)t).PropertyChanged -= handler).Where(args => args.EventArgs.PropertyName == property.Name).Select(_ => Unit.Default); + }; } internal static Func CreateValueAccessor(this MemberExpression source) { - //create an expression which accepts the parent and returns the child + // create an expression which accepts the parent and returns the child var property = source.GetProperty(); var method = property.GetMethod; - //convert the parameter i.e. the declaring class to an object + if (method is null) + { + throw new ArgumentException("The property does not have a valid get method.", nameof(source)); + } + + if (source.Expression is null) + { + throw new ArgumentException("The source expression does not have a valid expression.", nameof(source)); + } + + // convert the parameter i.e. the declaring class to an object var parameter = Expression.Parameter(typeof(object)); var converted = Expression.Convert(parameter, source.Expression.Type); - //call the get value of the property and box it + // call the get value of the property and box it var propertyCall = Expression.Call(converted, method); var boxed = Expression.Convert(propertyCall, typeof(object)); var accessorExpr = Expression.Lambda>(boxed, parameter); - var accessor = accessorExpr.Compile(); - return accessor; + return accessorExpr.Compile(); } - internal static Func> CreatePropertyChangedFactory(this MemberExpression source) + internal static MemberInfo GetMember(this Expression> expression) { - var property = source.GetProperty(); - var inpc = typeof(INotifyPropertyChanged).GetTypeInfo().IsAssignableFrom(property.DeclaringType.GetTypeInfo()); + if (expression is null) + { + throw new ArgumentException("Not a property expression"); + } - return t => + return GetMemberInfo(expression); + } + + internal static IEnumerable GetMemberChain(this Expression> expression) + { + var memberExpression = expression.Body as MemberExpression; + while (memberExpression?.Expression is not null) { - if (t == null) + if (memberExpression.Expression.NodeType != ExpressionType.Parameter) { - return Observable.Never; + var parent = memberExpression.Expression; + yield return memberExpression.Update(Expression.Parameter(parent.Type)); } - - if (!inpc) + else { - return Observable.Return(Unit.Default); + yield return memberExpression; } - return Observable.FromEventPattern - ( - handler => ((INotifyPropertyChanged) t).PropertyChanged += handler, - handler => ((INotifyPropertyChanged) t).PropertyChanged -= handler - ) - .Where(args => args.EventArgs.PropertyName == property.Name) - .Select(args => Unit.Default); - }; + memberExpression = memberExpression.Expression as MemberExpression; + } } internal static PropertyInfo GetProperty(this Expression> expression) { var property = expression.GetMember() as PropertyInfo; - if (property == null) + if (property is null) { throw new ArgumentException("Not a property expression"); } @@ -122,7 +123,7 @@ internal static PropertyInfo GetProperty(this Expression(this Expression> expression) + internal static string ToCacheKey(this Expression> expression) + where TObject : INotifyPropertyChanged { - if (expression == null) + var members = expression.GetMembers(); + + IEnumerable GetNames() { - throw new ArgumentException("Not a property expression"); + yield return typeof(TObject).FullName; + foreach (var member in members.Reverse()) + { + yield return member.Member.Name; + } } - return GetMemberInfo(expression); + return string.Join(".", GetNames()); } private static MemberInfo GetMemberInfo(LambdaExpression lambda) { - if (lambda == null) + if (lambda is null) { throw new ArgumentException("Not a property expression"); } - MemberExpression memberExpression = null; - if (lambda.Body.NodeType == ExpressionType.Convert) + MemberExpression? memberExpression = null; + switch (lambda.Body.NodeType) { - memberExpression = ((UnaryExpression)lambda.Body).Operand as MemberExpression; - } - else if (lambda.Body.NodeType == ExpressionType.MemberAccess) - { - memberExpression = lambda.Body as MemberExpression; - } - else if (lambda.Body.NodeType == ExpressionType.Call) - { - return ((MethodCallExpression)lambda.Body).Method; + case ExpressionType.Convert when lambda.Body is UnaryExpression { Operand: MemberExpression unaryMemberExpression }: + memberExpression = unaryMemberExpression; + break; + case ExpressionType.MemberAccess: + memberExpression = lambda.Body as MemberExpression; + break; + case ExpressionType.Call: + return ((MethodCallExpression)lambda.Body).Method; } - if (memberExpression == null) + if (memberExpression is null) { throw new ArgumentException("Not a member access"); } @@ -169,5 +176,4 @@ private static MemberInfo GetMemberInfo(LambdaExpression lambda) return memberExpression.Member; } } -} - +} \ No newline at end of file diff --git a/src/DynamicData/Binding/IEvaluateAware.cs b/src/DynamicData/Binding/IEvaluateAware.cs index 233533cab..3f546aeb9 100644 --- a/src/DynamicData/Binding/IEvaluateAware.cs +++ b/src/DynamicData/Binding/IEvaluateAware.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,13 +6,13 @@ namespace DynamicData.Binding { /// /// Implement on an object and use in conjunction with InvokeEvaluate operator - /// to make an object aware of any evaluates + /// to make an object aware of any evaluates. /// public interface IEvaluateAware { /// - /// Refresh method + /// Refresh method. /// void Evaluate(); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/IIndexAware.cs b/src/DynamicData/Binding/IIndexAware.cs index a8d318e55..cd50fe274 100644 --- a/src/DynamicData/Binding/IIndexAware.cs +++ b/src/DynamicData/Binding/IIndexAware.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,7 +6,7 @@ namespace DynamicData.Binding { /// /// Implement on an object and use in conjunction with UpdateIndex operator - /// to make an object aware of it's sorted index + /// to make an object aware of it's sorted index. /// public interface IIndexAware { @@ -18,4 +18,4 @@ public interface IIndexAware /// int Index { get; set; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/INotifyCollectionChangedSuspender.cs b/src/DynamicData/Binding/INotifyCollectionChangedSuspender.cs index 13030582a..ccdbdfe0f 100644 --- a/src/DynamicData/Binding/INotifyCollectionChangedSuspender.cs +++ b/src/DynamicData/Binding/INotifyCollectionChangedSuspender.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -12,13 +12,15 @@ namespace DynamicData.Binding public interface INotifyCollectionChangedSuspender { /// - /// Suspends notifications. When disposed, a reset notification is fired + /// Suspends count notifications. /// - IDisposable SuspendNotifications(); + /// A disposable which when disposed re-activates count notifications. + IDisposable SuspendCount(); /// - /// Suspends count notifications + /// Suspends notifications. When disposed, a reset notification is fired. /// - IDisposable SuspendCount(); + /// A disposable which when disposed re-activates notifications. + IDisposable SuspendNotifications(); } } \ No newline at end of file diff --git a/src/DynamicData/Binding/IObservableCollection.cs b/src/DynamicData/Binding/IObservableCollection.cs index 651eca7b5..d0a608f18 100644 --- a/src/DynamicData/Binding/IObservableCollection.cs +++ b/src/DynamicData/Binding/IObservableCollection.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,14 +9,16 @@ namespace DynamicData.Binding { /// - /// An override of observable collection which allows the suspension of notifications + /// An override of observable collection which allows the suspension of notifications. /// - /// - public interface IObservableCollection : INotifyCollectionChanged, - INotifyPropertyChanged, - IList, - INotifyCollectionChangedSuspender + /// The type of the item. + public interface IObservableCollection : INotifyCollectionChanged, INotifyPropertyChanged, IList, INotifyCollectionChangedSuspender { + /// + /// Clears the list and Loads the specified items. + /// + /// The items. + void Load(IEnumerable items); /// /// Moves the item at the specified index to a new location in the collection. @@ -24,11 +26,5 @@ public interface IObservableCollection : INotifyCollectionChanged, /// The zero-based index specifying the location of the item to be moved. /// The zero-based index specifying the new location of the item. void Move(int oldIndex, int newIndex); - - /// - /// Clears the list and Loads the specified items. - /// - /// The items. - void Load(IEnumerable items); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/IObservableCollectionAdaptor.cs b/src/DynamicData/Binding/IObservableCollectionAdaptor.cs index 1f2e3ae7b..46c9a58b3 100644 --- a/src/DynamicData/Binding/IObservableCollectionAdaptor.cs +++ b/src/DynamicData/Binding/IObservableCollectionAdaptor.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,17 +6,18 @@ namespace DynamicData.Binding { /// /// Represents an adaptor which is used to update observable collection from - /// a changeset stream + /// a change set stream. /// /// The type of the object. /// The type of the key. public interface IObservableCollectionAdaptor + where TKey : notnull { /// - /// Maintains the specified collection from the changes + /// Maintains the specified collection from the changes. /// /// The changes. /// The collection. void Adapt(IChangeSet changes, IObservableCollection collection); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/IObservableListEx.cs b/src/DynamicData/Binding/IObservableListEx.cs index e40ec3712..69bb3c133 100644 --- a/src/DynamicData/Binding/IObservableListEx.cs +++ b/src/DynamicData/Binding/IObservableListEx.cs @@ -1,4 +1,8 @@ -using System; +// Copyright (c) 2011-2020 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. + +using System; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; @@ -19,28 +23,24 @@ public static class IObservableListEx /// The type of the object. /// The source. /// The output observable list. - /// The changeset for continued chaining. - /// source - public static IObservable> BindToObservableList( - this IObservable> source, - out IObservableList observableList) + /// The change set for continued chaining. + /// source. + public static IObservable> BindToObservableList(this IObservable> source, out IObservableList observableList) { - if (source == null) throw new ArgumentNullException(nameof(source)); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } // Load our source list with the change set. - // Each changeset we need to convert to remove the key. + // Each change set we need to convert to remove the key. var sourceList = new SourceList(source); - // Output our readonly observable list, preventing the sourcelist from being editted from anywhere else. + // Output our readonly observable list, preventing the source list from being edited from anywhere else. observableList = sourceList; // Return a observable that will connect to the source so we can properly dispose when the pipeline ends. - return Observable.Create>(observer => - { - return source - .Finally(() => sourceList.Dispose()) - .SubscribeSafe(observer); - }); + return Observable.Create>(observer => { return source.Finally(() => sourceList.Dispose()).SubscribeSafe(observer); }); } /// @@ -53,29 +53,25 @@ public static IObservable> BindToObservableList( /// The type of the key. /// The source. /// The observable list which is the output. - /// The changeset for continued chaining. - /// source - public static IObservable> BindToObservableList( - this IObservable> source, - out IObservableList observableList) + /// The change set for continued chaining. + /// source. + public static IObservable> BindToObservableList(this IObservable> source, out IObservableList observableList) + where TKey : notnull { - if (source == null) throw new ArgumentNullException(nameof(source)); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } // Load our source list with the change set. - // Each changeset we need to convert to remove the key. + // Each change set we need to convert to remove the key. var sourceList = new SourceList(); - // Output our readonly observable list, preventing the sourcelist from being editted from anywhere else. + // Output our readonly observable list, preventing the source list from being edited from anywhere else. observableList = sourceList; // Return a observable that will connect to the source so we can properly dispose when the pipeline ends. - return Observable.Create>(observer => - { - return source - .Do(changes => sourceList.Edit(editor => editor.Clone(changes.RemoveKey(editor)))) - .Finally(() => sourceList.Dispose()) - .SubscribeSafe(observer); - }); + return Observable.Create>(observer => { return source.Do(changes => sourceList.Edit(editor => editor.Clone(changes.RemoveKey(editor)))).Finally(() => sourceList.Dispose()).SubscribeSafe(observer); }); } /// @@ -88,49 +84,53 @@ public static IObservable> BindToObservableListThe type of the key. /// The source. /// The output observable list. - /// The changeset for continued chaining. - /// source - public static IObservable> BindToObservableList( - this IObservable> source, - out IObservableList observableList) + /// The change set for continued chaining. + /// source. + public static IObservable> BindToObservableList(this IObservable> source, out IObservableList observableList) + where TKey : notnull { - if (source == null) throw new ArgumentNullException(nameof(source)); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } // Load our source list with the change set. - // Each changeset we need to convert to remove the key. + // Each change set we need to convert to remove the key. var sourceList = new SourceList(); - // Output our readonly observable list, preventing the sourcelist from being editted from anywhere else. + // Output our readonly observable list, preventing the source list from being edited from anywhere else. observableList = sourceList; // Return a observable that will connect to the source so we can properly dispose when the pipeline ends. - return Observable.Create>(observer => - { - return source - .Do(changes => + return Observable.Create>( + observer => { - switch (changes.SortedItems.SortReason) - { - case SortReason.InitialLoad: - sourceList.AddRange(changes.SortedItems.Select(kv => kv.Value)); - break; - case SortReason.ComparerChanged: - case SortReason.DataChanged: - case SortReason.Reorder: - sourceList.Edit(editor => editor.Clone(changes.RemoveKey(editor))); - break; - case SortReason.Reset: - sourceList.Edit(editor => + return source.Do( + changes => { - editor.Clear(); - editor.AddRange(changes.SortedItems.Select(kv => kv.Value)); - }); - break; - } - }) - .Finally(() => sourceList.Dispose()) - .SubscribeSafe(observer); - }); + switch (changes.SortedItems.SortReason) + { + case SortReason.InitialLoad: + sourceList.AddRange(changes.SortedItems.Select(kv => kv.Value)); + break; + + case SortReason.ComparerChanged: + case SortReason.DataChanged: + case SortReason.Reorder: + sourceList.Edit(editor => editor.Clone(changes.RemoveKey(editor))); + break; + + case SortReason.Reset: + sourceList.Edit( + editor => + { + editor.Clear(); + editor.AddRange(changes.SortedItems.Select(kv => kv.Value)); + }); + break; + } + }).Finally(() => sourceList.Dispose()).SubscribeSafe(observer); + }); } /// @@ -139,10 +139,11 @@ public static IObservable> BindToObservableList< /// /// The type of the object. /// The type of the key. - /// The source change set - /// The list needed to support refresh - /// The downcasted + /// The source change set. + /// The list needed to support refresh. + /// The down casted . private static IChangeSet RemoveKey(this IChangeSet changeSetWithKey, IExtendedList list) + where TKey : notnull { var enumerator = new Cache.Internal.RemoveKeyEnumerator(changeSetWithKey, list); diff --git a/src/DynamicData/Binding/ISortedObservableCollectionAdaptor.cs b/src/DynamicData/Binding/ISortedObservableCollectionAdaptor.cs index 3488842ba..6b8d71be4 100644 --- a/src/DynamicData/Binding/ISortedObservableCollectionAdaptor.cs +++ b/src/DynamicData/Binding/ISortedObservableCollectionAdaptor.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,17 +6,18 @@ namespace DynamicData.Binding { /// /// Represents an adaptor which is used to update observable collection from - /// a sorted change set stream + /// a sorted change set stream. /// /// The type of the object. /// The type of the key. public interface ISortedObservableCollectionAdaptor + where TKey : notnull { /// - /// Maintains the specified collection from the changes + /// Maintains the specified collection from the changes. /// /// The changes. /// The collection. void Adapt(ISortedChangeSet changes, IObservableCollection collection); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/NotifyPropertyChangedEx.cs b/src/DynamicData/Binding/NotifyPropertyChangedEx.cs index a7c507811..4a6f3f2b1 100644 --- a/src/DynamicData/Binding/NotifyPropertyChangedEx.cs +++ b/src/DynamicData/Binding/NotifyPropertyChangedEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,12 +7,11 @@ using System.Linq; using System.Linq.Expressions; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.Binding { /// - /// Property changes notification + /// Property changes notification. /// public static class NotifyPropertyChangedEx { @@ -21,108 +20,47 @@ public static class NotifyPropertyChangedEx /// /// The type of the object. /// The source. - /// specify properties to Monitor, or omit to monitor all property changes + /// specify properties to Monitor, or omit to monitor all property changes. /// A observable which includes notifying on any property. - /// - public static IObservable WhenAnyPropertyChanged([NotNull] this TObject source, params string[] propertiesToMonitor) + public static IObservable WhenAnyPropertyChanged(this TObject source, params string[] propertiesToMonitor) where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return Observable.FromEventPattern - ( - handler => source.PropertyChanged += handler, - handler => source.PropertyChanged -= handler - ) - .Where(x => propertiesToMonitor == null || propertiesToMonitor.Length == 0 || propertiesToMonitor.Contains(x.EventArgs.PropertyName)) - .Select(x => source); - } - - /// - /// Observes property changes for the specified property, starting with the current value - /// - /// The type of the object. - /// The type of the value. - /// The source. - /// The property to observe - /// If true the resulting observable includes the initial value - /// A fallback value may be specified to ensure a notification is received when a value is unobtainable. - /// For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. - /// For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. - /// A observable which also notifies when the property value changes. - /// propertyAccessor - public static IObservable> WhenPropertyChanged([NotNull] this TObject source, - Expression> propertyAccessor, - bool notifyOnInitialValue = true, - Func fallbackValue = null) - where TObject : INotifyPropertyChanged - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (propertyAccessor == null) - { - throw new ArgumentNullException(nameof(propertyAccessor)); - } - - var cache = ObservablePropertyFactoryCache.Instance.GetFactory(propertyAccessor); - return cache.Create(source, notifyOnInitialValue) - .Where(pv => !pv.UnobtainableValue || (pv.UnobtainableValue && fallbackValue != null)); - } - - /// - /// Observes property changes for the specified property, starting with the current value - /// - /// The source. - /// The property to observe - /// If true the resulting observable includes the initial value - /// A fallback value may be specified to ensure a notification is received when a value is unobtainable. - /// For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. - /// For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. - public static IObservable WhenValueChanged([NotNull] this TObject source, Expression> propertyAccessor, bool notifyOnInitialValue = true, Func fallbackValue = null) - where TObject : INotifyPropertyChanged - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (propertyAccessor == null) - { - throw new ArgumentNullException(nameof(propertyAccessor)); - } - - return source.WhenChanged(propertyAccessor, notifyOnInitialValue, fallbackValue); + return Observable.FromEventPattern(handler => source.PropertyChanged += handler, handler => source.PropertyChanged -= handler).Where(x => propertiesToMonitor.Length == 0 || propertiesToMonitor.Contains(x.EventArgs.PropertyName)).Select(_ => source); } /// /// Produces an observable based on the combined values of the specified properties, including the initial value. /// ** A fallback value may be specified to ensure a notification is received when a value is unobtainable. - /// For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. - /// For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. + /// For example when observing Parent.Child.Age, if Child is null the value is unobtainable as Age is a struct and cannot be set to Null. + /// For an object like Parent.Child.Sibling, sibling is an object so if Child is null, the value null and obtainable and is returned as null. /// - public static IObservable WhenChanged([NotNull] this TObject source, - Expression> p1, - Func resultSelector, - Func p1Fallback = null) + /// The type of the object. + /// The type of the result. + /// The type of the first property. + /// The source object. + /// An expression to the first property. + /// A function which will select the result from the properties and the source. + /// Provides a fall back value for the first property in case of the property value cannot be obtained. + /// An observable which emits the results. + public static IObservable WhenChanged(this TObject source, Expression> p1, Func resultSelector, Func? p1Fallback = null) where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (p1 == null) + if (p1 is null) { throw new ArgumentNullException(nameof(p1)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } @@ -133,308 +71,354 @@ public static IObservable WhenChanged([No /// /// Produces an observable based on the combined values of the specified properties, including the initial value. /// ** A fallback value may be specified to ensure a notification is received when a value is unobtainable. - /// For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. - /// For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. + /// For example when observing Parent.Child.Age, if Child is null the value is unobtainable as Age is a struct and cannot be set to Null. + /// For an object like Parent.Child.Sibling, sibling is an object so if Child is null, the value null and obtainable and is returned as null. /// - public static IObservable WhenChanged([NotNull] this TObject source, - Expression> p1, - Expression> p2, - Func resultSelector, - Func p1Fallback = null, - Func p2Fallback = null) - where TObject : INotifyPropertyChanged + /// The type of the object. + /// The type of the result. + /// The type of the first property. + /// The type of the second property. + /// The source object. + /// An expression to the first property. + /// An expression to the second property. + /// A function which will select the result from the properties and the source. + /// Provides a fall back value for the first property in case of the property value cannot be obtained. + /// Provides a fall back value for the second property in case of the property value cannot be obtained. + /// An observable which emits the results. + public static IObservable WhenChanged(this TObject source, Expression> p1, Expression> p2, Func resultSelector, Func? p1Fallback = null, Func? p2Fallback = null) + where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (p1 == null) + if (p1 is null) { throw new ArgumentNullException(nameof(p1)); } - if (p2 == null) + if (p2 is null) { throw new ArgumentNullException(nameof(p2)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return Observable.CombineLatest - ( - source.WhenChanged(p1, true, p1Fallback), - source.WhenChanged(p2, true, p2Fallback), - (v1, v2) => resultSelector(source, v1, v2) - ); + return source.WhenChanged(p1, true, p1Fallback).CombineLatest(source.WhenChanged(p2, true, p2Fallback), (v1, v2) => resultSelector(source, v1, v2)); } /// /// Produces an observable based on the combined values of the specified properties, including the initial value. /// ** A fallback value may be specified to ensure a notification is received when a value is unobtainable. - /// For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. - /// For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. + /// For example when observing Parent.Child.Age, if Child is null the value is unobtainable as Age is a struct and cannot be set to Null. + /// For an object like Parent.Child.Sibling, sibling is an object so if Child is null, the value null and obtainable and is returned as null. /// - public static IObservable WhenChanged([NotNull] this TObject source, - Expression> p1, - Expression> p2, - Expression> p3, - Func resultSelector, - Func p1Fallback = null, - Func p2Fallback = null, - Func p3Fallback = null) + /// The type of the object. + /// The type of the result. + /// The type of the first property. + /// The type of the second property. + /// The type of the third property. + /// The source object. + /// An expression to the first property. + /// An expression to the second property. + /// An expression to the third property. + /// A function which will select the result from the properties and the source. + /// Provides a fall back value for the first property in case of the property value cannot be obtained. + /// Provides a fall back value for the second property in case of the property value cannot be obtained. + /// Provides a fall back value for the third property in case of the property value cannot be obtained. + /// An observable which emits the results. + public static IObservable WhenChanged(this TObject source, Expression> p1, Expression> p2, Expression> p3, Func resultSelector, Func? p1Fallback = null, Func? p2Fallback = null, Func? p3Fallback = null) where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (p1 == null) + if (p1 is null) { throw new ArgumentNullException(nameof(p1)); } - if (p2 == null) + if (p2 is null) { throw new ArgumentNullException(nameof(p2)); } - if (p3 == null) + if (p3 is null) { throw new ArgumentNullException(nameof(p3)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return Observable.CombineLatest - ( - source.WhenChanged(p1, true, p1Fallback), - source.WhenChanged(p2, true, p2Fallback), - source.WhenChanged(p3, true, p3Fallback), - (v1, v2, v3) => resultSelector(source, v1, v2, v3) - ); + return source.WhenChanged(p1, true, p1Fallback).CombineLatest(source.WhenChanged(p2, true, p2Fallback), source.WhenChanged(p3, true, p3Fallback), (v1, v2, v3) => resultSelector(source, v1, v2, v3)); } /// /// Produces an observable based on the combined values of the specified properties, including the initial value. /// ** A fallback value may be specified to ensure a notification is received when a value is unobtainable. - /// For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. - /// For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. + /// For example when observing Parent.Child.Age, if Child is null the value is unobtainable as Age is a struct and cannot be set to Null. + /// For an object like Parent.Child.Sibling, sibling is an object so if Child is null, the value null and obtainable and is returned as null. /// - public static IObservable WhenChanged([NotNull] this TObject source, - Expression> p1, - Expression> p2, - Expression> p3, - Expression> p4, - Func resultSelector, - Func p1Fallback = null, - Func p2Fallback = null, - Func p3Fallback = null, - Func p4Fallback = null) + /// The type of the object. + /// The type of the result. + /// The type of the first property. + /// The type of the second property. + /// The type of the third property. + /// The type of the fourth property. + /// The source object. + /// An expression to the first property. + /// An expression to the second property. + /// An expression to the third property. + /// An expression to the fourth property. + /// A function which will select the result from the properties and the source. + /// Provides a fall back value for the first property in case of the property value cannot be obtained. + /// Provides a fall back value for the second property in case of the property value cannot be obtained. + /// Provides a fall back value for the third property in case of the property value cannot be obtained. + /// Provides a fall back value for the fourth property in case of the property value cannot be obtained. + /// An observable which emits the results. + public static IObservable WhenChanged(this TObject source, Expression> p1, Expression> p2, Expression> p3, Expression> p4, Func resultSelector, Func? p1Fallback = null, Func? p2Fallback = null, Func? p3Fallback = null, Func? p4Fallback = null) where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (p1 == null) + if (p1 is null) { throw new ArgumentNullException(nameof(p1)); } - if (p2 == null) + if (p2 is null) { throw new ArgumentNullException(nameof(p2)); } - if (p3 == null) + if (p3 is null) { throw new ArgumentNullException(nameof(p3)); } - if (p4 == null) + if (p4 is null) { throw new ArgumentNullException(nameof(p4)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return Observable.CombineLatest - ( - source.WhenChanged(p1, true, p1Fallback), - source.WhenChanged(p2, true, p2Fallback), - source.WhenChanged(p3, true, p3Fallback), - source.WhenChanged(p4, true, p4Fallback), - (v1, v2, v3, v4) => resultSelector(source, v1, v2, v3, v4) - ); + return source.WhenChanged(p1, true, p1Fallback).CombineLatest(source.WhenChanged(p2, true, p2Fallback), source.WhenChanged(p3, true, p3Fallback), source.WhenChanged(p4, true, p4Fallback), (v1, v2, v3, v4) => resultSelector(source, v1, v2, v3, v4)); } /// /// Produces an observable based on the combined values of the specified properties, including the initial value. /// ** A fallback value may be specified to ensure a notification is received when a value is unobtainable. - /// For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. - /// For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. + /// For example when observing Parent.Child.Age, if Child is null the value is unobtainable as Age is a struct and cannot be set to Null. + /// For an object like Parent.Child.Sibling, sibling is an object so if Child is null, the value null and obtainable and is returned as null. /// - public static IObservable WhenChanged([NotNull] this TObject source, - Expression> p1, - Expression> p2, - Expression> p3, - Expression> p4, - Expression> p5, - Func resultSelector, - Func p1Fallback = null, - Func p2Fallback = null, - Func p3Fallback = null, - Func p4Fallback = null, - Func p5Fallback = null) + /// The type of the object. + /// The type of the result. + /// The type of the first property. + /// The type of the second property. + /// The type of the third property. + /// The type of the fourth property. + /// The type of the fifth property. + /// The source object. + /// An expression to the first property. + /// An expression to the second property. + /// An expression to the third property. + /// An expression to the fourth property. + /// An expression to the fifth property. + /// A function which will select the result from the properties and the source. + /// Provides a fall back value for the first property in case of the property value cannot be obtained. + /// Provides a fall back value for the second property in case of the property value cannot be obtained. + /// Provides a fall back value for the third property in case of the property value cannot be obtained. + /// Provides a fall back value for the fourth property in case of the property value cannot be obtained. + /// Provides a fall back value for the fifth property in case of the property value cannot be obtained. + /// An observable which emits the results. + public static IObservable WhenChanged(this TObject source, Expression> p1, Expression> p2, Expression> p3, Expression> p4, Expression> p5, Func resultSelector, Func? p1Fallback = null, Func? p2Fallback = null, Func? p3Fallback = null, Func? p4Fallback = null, Func? p5Fallback = null) where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (p1 == null) + if (p1 is null) { throw new ArgumentNullException(nameof(p1)); } - if (p2 == null) + if (p2 is null) { throw new ArgumentNullException(nameof(p2)); } - if (p3 == null) + if (p3 is null) { throw new ArgumentNullException(nameof(p3)); } - if (p4 == null) + if (p4 is null) { throw new ArgumentNullException(nameof(p4)); } - if (p5 == null) + if (p5 is null) { throw new ArgumentNullException(nameof(p5)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return Observable.CombineLatest - ( - source.WhenChanged(p1, true, p1Fallback), - source.WhenChanged(p2, true, p2Fallback), - source.WhenChanged(p3, true, p3Fallback), - source.WhenChanged(p4, true, p4Fallback), - source.WhenChanged(p5, true, p5Fallback), - (v1, v2, v3, v4, v5) => resultSelector(source, v1, v2, v3, v4, v5) - ); + return source.WhenChanged(p1, true, p1Fallback).CombineLatest(source.WhenChanged(p2, true, p2Fallback), source.WhenChanged(p3, true, p3Fallback), source.WhenChanged(p4, true, p4Fallback), source.WhenChanged(p5, true, p5Fallback), (v1, v2, v3, v4, v5) => resultSelector(source, v1, v2, v3, v4, v5)); } /// /// Produces an observable based on the combined values of the specified properties, including the initial value. /// ** A fallback value may be specified to ensure a notification is received when a value is unobtainable. - /// For example when observing Parent.Child.Age, if Child == null the value is unobtainable as Age is a struct and cannot be set to Null. - /// For an object like Parent.Child.Sibling, sibling is an object so if Child == null, the value null and obtainable and is returned as null. + /// For example when observing Parent.Child.Age, if Child is null the value is unobtainable as Age is a struct and cannot be set to Null. + /// For an object like Parent.Child.Sibling, sibling is an object so if Child is null, the value null and obtainable and is returned as null. /// - public static IObservable WhenChanged([NotNull] this TObject source, - Expression> p1, - Expression> p2, - Expression> p3, - Expression> p4, - Expression> p5, - Expression> p6, - Func resultSelector, - Func p1Fallback = null, - Func p2Fallback = null, - Func p3Fallback = null, - Func p4Fallback = null, - Func p5Fallback = null, - Func p6Fallback = null) + /// The type of the object. + /// The type of the result. + /// The type of the first property. + /// The type of the second property. + /// The type of the third property. + /// The type of the fourth property. + /// The type of the fifth property. + /// The type of the sixth property. + /// The source object. + /// An expression to the first property. + /// An expression to the second property. + /// An expression to the third property. + /// An expression to the fourth property. + /// An expression to the fifth property. + /// An expression to the sixth property. + /// A function which will select the result from the properties and the source. + /// Provides a fall back value for the first property in case of the property value cannot be obtained. + /// Provides a fall back value for the second property in case of the property value cannot be obtained. + /// Provides a fall back value for the third property in case of the property value cannot be obtained. + /// Provides a fall back value for the fourth property in case of the property value cannot be obtained. + /// Provides a fall back value for the fifth property in case of the property value cannot be obtained. + /// Provides a fall back value for the sixth property in case of the property value cannot be obtained. + /// An observable which emits the results. + public static IObservable WhenChanged(this TObject source, Expression> p1, Expression> p2, Expression> p3, Expression> p4, Expression> p5, Expression> p6, Func resultSelector, Func? p1Fallback = null, Func? p2Fallback = null, Func? p3Fallback = null, Func? p4Fallback = null, Func? p5Fallback = null, Func? p6Fallback = null) where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (p1 == null) + if (p1 is null) { throw new ArgumentNullException(nameof(p1)); } - if (p2 == null) + if (p2 is null) { throw new ArgumentNullException(nameof(p2)); } - if (p3 == null) + if (p3 is null) { throw new ArgumentNullException(nameof(p3)); } - if (p4 == null) + if (p4 is null) { throw new ArgumentNullException(nameof(p4)); } - if (p5 == null) + if (p5 is null) { throw new ArgumentNullException(nameof(p5)); } - if (p6 == null) + if (p6 is null) { throw new ArgumentNullException(nameof(p6)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return Observable.CombineLatest - ( - source.WhenChanged(p1, true, p1Fallback), - source.WhenChanged(p2, true, p2Fallback), - source.WhenChanged(p3, true, p3Fallback), - source.WhenChanged(p4, true, p4Fallback), - source.WhenChanged(p5, true, p5Fallback), - source.WhenChanged(p6, true, p6Fallback), - (v1, v2, v3, v4, v5,v6) => resultSelector(source, v1, v2, v3, v4, v5, v6) - ); + return source.WhenChanged(p1, true, p1Fallback).CombineLatest(source.WhenChanged(p2, true, p2Fallback), source.WhenChanged(p3, true, p3Fallback), source.WhenChanged(p4, true, p4Fallback), source.WhenChanged(p5, true, p5Fallback), source.WhenChanged(p6, true, p6Fallback), (v1, v2, v3, v4, v5, v6) => resultSelector(source, v1, v2, v3, v4, v5, v6)); } - internal static IObservable WhenChanged(this TObject source, Expression> expression, bool notifyInitial = true, Func fallbackValue = null) + /// + /// Observes property changes for the specified property, starting with the current value. + /// + /// The type of the object. + /// The type of the value. + /// The source. + /// The property to observe. + /// If true the resulting observable includes the initial value. + /// A fallback value may be specified to ensure a notification is received when a value is unobtainable. + /// For example when observing Parent.Child.Age, if Child is null the value is unobtainable as Age is a struct and cannot be set to Null. + /// For an object like Parent.Child.Sibling, sibling is an object so if Child is null, the value null and obtainable and is returned as null. + /// A observable which also notifies when the property value changes. + /// propertyAccessor. + public static IObservable> WhenPropertyChanged(this TObject source, Expression> propertyAccessor, bool notifyOnInitialValue = true, Func? fallbackValue = null) where TObject : INotifyPropertyChanged { - var factory = ObservablePropertyFactoryCache.Instance.GetFactory(expression); - return factory.Create(source, notifyInitial) - .Where(pv => !pv.UnobtainableValue || pv.UnobtainableValue && fallbackValue != null) - .Select(pv => - { - if (pv.UnobtainableValue && fallbackValue != null) - { - return fallbackValue(); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (propertyAccessor is null) + { + throw new ArgumentNullException(nameof(propertyAccessor)); + } + + var cache = ObservablePropertyFactoryCache.Instance.GetFactory(propertyAccessor); + return cache.Create(source, notifyOnInitialValue).Where(pv => !pv.UnobtainableValue || (pv.UnobtainableValue && fallbackValue is not null)); + } + + /// + /// Observes property changes for the specified property, starting with the current value. + /// + /// The type of the object. + /// The type of the first property. + /// The source. + /// The property to observe. + /// If true the resulting observable includes the initial value. + /// A fallback value may be specified to ensure a notification is received when a value is unobtainable. + /// For example when observing Parent.Child.Age, if Child is null the value is unobtainable as Age is a struct and cannot be set to Null. + /// For an object like Parent.Child.Sibling, sibling is an object so if Child is null, the value null and obtainable and is returned as null. + /// An observable which emits the results. + public static IObservable WhenValueChanged(this TObject source, Expression> propertyAccessor, bool notifyOnInitialValue = true, Func? fallbackValue = null) + where TObject : INotifyPropertyChanged + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - return pv.Value; - }); + if (propertyAccessor is null) + { + throw new ArgumentNullException(nameof(propertyAccessor)); + } + + return source.WhenChanged(propertyAccessor, notifyOnInitialValue, fallbackValue); } internal static Func>> GetFactory(this Expression> expression) @@ -444,5 +428,20 @@ internal static Func factory.Create(t, initial); } + internal static IObservable WhenChanged(this TObject source, Expression> expression, bool notifyInitial = true, Func? fallbackValue = null) + where TObject : INotifyPropertyChanged + { + var factory = ObservablePropertyFactoryCache.Instance.GetFactory(expression); + return factory.Create(source, notifyInitial).Where(pv => !pv.UnobtainableValue || (pv.UnobtainableValue && fallbackValue is not null)).Select( + pv => + { + if (pv.UnobtainableValue && fallbackValue is not null) + { + return fallbackValue(); + } + + return pv.Value; + }); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/Observable.cs b/src/DynamicData/Binding/Observable.cs new file mode 100644 index 000000000..744f9732c --- /dev/null +++ b/src/DynamicData/Binding/Observable.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2011-2020 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. + +using System; +using System.Reactive.Linq; + +namespace DynamicData.Binding +{ + internal static class Observable + { + public static IObservable Default { get; } = Observable.Return(default); + + public static IObservable Empty { get; } = Observable.Empty(); + + public static IObservable Never { get; } = Observable.Never(); + } +} \ No newline at end of file diff --git a/src/DynamicData/Binding/ObservableCollectionAdaptor.cs b/src/DynamicData/Binding/ObservableCollectionAdaptor.cs index 0cfadb8df..eb6efe919 100644 --- a/src/DynamicData/Binding/ObservableCollectionAdaptor.cs +++ b/src/DynamicData/Binding/ObservableCollectionAdaptor.cs @@ -1,42 +1,45 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; -using DynamicData.Annotations; +using System.Diagnostics.CodeAnalysis; + using DynamicData.Cache.Internal; namespace DynamicData.Binding { /// - /// Adaptor to reflect a change set into an observable list + /// Adaptor to reflect a change set into an observable list. /// - /// + /// The type of the item. public class ObservableCollectionAdaptor : IChangeSetAdaptor { private readonly IObservableCollection _collection; + private readonly int _refreshThreshold; + private bool _loaded; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The collection. /// The refresh threshold. - /// collection - public ObservableCollectionAdaptor([NotNull] IObservableCollection collection, int refreshThreshold = 25) + /// collection. + public ObservableCollectionAdaptor(IObservableCollection collection, int refreshThreshold = 25) { _collection = collection ?? throw new ArgumentNullException(nameof(collection)); _refreshThreshold = refreshThreshold; } /// - /// Maintains the specified collection from the changes + /// Maintains the specified collection from the changes. /// /// The changes. public void Adapt(IChangeSet changes) { - if (changes == null) + if (changes is null) { throw new ArgumentNullException(nameof(changes)); } @@ -58,38 +61,42 @@ public void Adapt(IChangeSet changes) /// /// Represents an adaptor which is used to update observable collection from - /// a changeset stream + /// a change set stream. /// /// The type of the object. /// The type of the key. + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Same class name, only generic difference.")] public class ObservableCollectionAdaptor : IObservableCollectionAdaptor + where TKey : notnull { + private readonly Cache _cache = new(); + private readonly int _refreshThreshold; - private bool _loaded; - private readonly Cache _cache = new Cache(); + private bool _loaded; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The threshold before a reset notification is triggered. public ObservableCollectionAdaptor(int refreshThreshold = 25) { _refreshThreshold = refreshThreshold; } /// - /// Maintains the specified collection from the changes + /// Maintains the specified collection from the changes. /// /// The changes. /// The collection. public void Adapt(IChangeSet changes, IObservableCollection collection) { - if (changes == null) + if (changes is null) { throw new ArgumentNullException(nameof(changes)); } - if (collection == null) + if (collection is null) { throw new ArgumentNullException(nameof(collection)); } @@ -126,6 +133,7 @@ private static void DoUpdate(IChangeSet updates, IObservableColle case ChangeReason.Remove: list.Remove(update.Current); break; + case ChangeReason.Update: list.Replace(update.Previous.Value, update.Current); break; @@ -133,4 +141,4 @@ private static void DoUpdate(IChangeSet updates, IObservableColle } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/ObservableCollectionEx.cs b/src/DynamicData/Binding/ObservableCollectionEx.cs index 37d6cb10b..179331938 100644 --- a/src/DynamicData/Binding/ObservableCollectionEx.cs +++ b/src/DynamicData/Binding/ObservableCollectionEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -13,26 +13,36 @@ namespace DynamicData.Binding { /// - /// Extensions to convert an observable collection into a dynamic stream + /// Extensions to convert an observable collection into a dynamic stream. /// public static class ObservableCollectionEx { + /// + /// Observes notify collection changed args. + /// + /// The source collection. + /// An observable that emits the event patterns. + public static IObservable> ObserveCollectionChanges(this INotifyCollectionChanged source) + { + return Observable.FromEventPattern(h => source.CollectionChanged += h, h => source.CollectionChanged -= h); + } + /// /// Convert an observable collection into an observable change set. /// Change set observes collection change events. /// /// The type of the object. /// The source. - /// - /// source + /// An observable that emits the change set. + /// source. public static IObservable> ToObservableChangeSet(this ObservableCollection source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return ToObservableChangeSet,T>(source); + return ToObservableChangeSet, T>(source); } /// @@ -43,18 +53,19 @@ public static IObservable> ToObservableChangeSet(this Observabl /// The type of the key. /// The source. /// The key selector. - /// + /// An observable that emits the change set. /// source /// or - /// keySelector + /// keySelector. public static IObservable> ToObservableChangeSet(this ObservableCollection source, Func keySelector) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } @@ -68,11 +79,11 @@ public static IObservable> ToObservableChangeSet /// The type of the object. /// The source. - /// - /// source + /// An observable that emits the change set. + /// source. public static IObservable> ToObservableChangeSet(this ReadOnlyObservableCollection source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -88,18 +99,19 @@ public static IObservable> ToObservableChangeSet(this ReadOnlyO /// The type of the key. /// The source. /// The key selector. - /// + /// An observable that emits the change set. /// source /// or - /// keySelector + /// keySelector. public static IObservable> ToObservableChangeSet(this ReadOnlyObservableCollection source, Func keySelector) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } @@ -111,99 +123,80 @@ public static IObservable> ToObservableChangeSet + /// The type of collection. /// The type of the object. - /// /// The source. - /// - /// source + /// An observable that emits the change set. + /// source. public static IObservable> ToObservableChangeSet(this TCollection source) where TCollection : INotifyCollectionChanged, IEnumerable { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return Observable.Create>(observer => - { - var data = new ChangeAwareList(source); - - if (data.Count > 0) - { - observer.OnNext(data.CaptureChanges()); - } - - return source.ObserveCollectionChanges() - .Scan(data, (list, args) => + return Observable.Create>( + observer => { - var changes = args.EventArgs; + var data = new ChangeAwareList(source); - switch (changes.Action) + if (data.Count > 0) { - case NotifyCollectionChangedAction.Add: - { - if (changes.NewItems.Count == 1) - { - list.Insert(changes.NewStartingIndex, (T) changes.NewItems[0]); - } - else - { - list.InsertRange(changes.NewItems.Cast(), changes.NewStartingIndex); - } - - break; - } - - case NotifyCollectionChangedAction.Remove: - { - if (changes.OldItems.Count == 1) - { - list.RemoveAt(changes.OldStartingIndex); - } - else - { - list.RemoveRange(changes.OldStartingIndex, changes.OldItems.Count); - } - - break; - } - - case NotifyCollectionChangedAction.Replace: - { - list[changes.NewStartingIndex] = (T) changes.NewItems[0]; - break; - } - - case NotifyCollectionChangedAction.Reset: - { - list.Clear(); - list.AddRange(source); - break; - } - - case NotifyCollectionChangedAction.Move: - { - list.Move(changes.OldStartingIndex, changes.NewStartingIndex); - break; - } + observer.OnNext(data.CaptureChanges()); } - return list; - }) - .Select(list => list.CaptureChanges()) - .SubscribeSafe(observer); - }); - } - - /// - /// Observes notify collection changed args - /// - public static IObservable> ObserveCollectionChanges(this INotifyCollectionChanged source) - { - return Observable - .FromEventPattern( - h => source.CollectionChanged += h, - h => source.CollectionChanged -= h); + return source.ObserveCollectionChanges().Scan( + data, + (list, args) => + { + var changes = args.EventArgs; + + switch (changes.Action) + { + case NotifyCollectionChangedAction.Add when changes.NewItems is not null: + { + if (changes.NewItems.Count == 1 && changes.NewItems[0] is T item) + { + list.Insert(changes.NewStartingIndex, item); + } + else + { + list.InsertRange(changes.NewItems.Cast(), changes.NewStartingIndex); + } + + break; + } + + case NotifyCollectionChangedAction.Remove when changes.OldItems is not null: + { + if (changes.OldItems.Count == 1) + { + list.RemoveAt(changes.OldStartingIndex); + } + else + { + list.RemoveRange(changes.OldStartingIndex, changes.OldItems.Count); + } + + break; + } + + case NotifyCollectionChangedAction.Replace when changes.NewItems?[0] is T replacedItem: + list[changes.NewStartingIndex] = replacedItem; + break; + case NotifyCollectionChangedAction.Reset: + list.Clear(); + list.AddRange(source); + break; + case NotifyCollectionChangedAction.Move: + list.Move(changes.OldStartingIndex, changes.NewStartingIndex); + break; + } + + return list; + }).Select(list => list.CaptureChanges()).SubscribeSafe(observer); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/ObservableCollectionExtended.cs b/src/DynamicData/Binding/ObservableCollectionExtended.cs index 916d00216..ac791c9f6 100644 --- a/src/DynamicData/Binding/ObservableCollectionExtended.cs +++ b/src/DynamicData/Binding/ObservableCollectionExtended.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -12,124 +12,85 @@ namespace DynamicData.Binding { /// - /// An override of observable collection which allows the suspension of notifications + /// An override of observable collection which allows the suspension of notifications. /// - /// + /// The type of the item. public class ObservableCollectionExtended : ObservableCollection, IObservableCollection, IExtendedList { - #region Construction + private bool _suspendCount; + + private bool _suspendNotifications; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public ObservableCollectionExtended() { } /// - /// Initializes a new instance of the class that contains elements copied from the specified list. + /// Initializes a new instance of the class that contains elements copied from the specified list. /// - /// The list from which the elements are copied.The parameter cannot be null. + /// The list from which the elements are copied.The parameter cannot be null. public ObservableCollectionExtended(List list) : base(list) { } /// - /// Initializes a new instance of the class that contains elements copied from the specified collection. + /// Initializes a new instance of the class that contains elements copied from the specified collection. /// - /// The collection from which the elements are copied.The parameter cannot be null. + /// The collection from which the elements are copied.The parameter cannot be null. public ObservableCollectionExtended(IEnumerable collection) : base(collection) { } - #endregion - - #region Implementation of IObservableCollection - - private bool _suspendNotifications; - private bool _suspendCount; - /// - /// Suspends notifications. When disposed, a reset notification is fired - /// - /// - public IDisposable SuspendNotifications() - { - _suspendCount = true; - _suspendNotifications = true; - - return Disposable.Create(() => - { - _suspendCount = false; - _suspendNotifications = false; - OnPropertyChanged(new PropertyChangedEventArgs("Count")); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - }); - } - - /// - /// Suspends count notifications - /// - /// - public IDisposable SuspendCount() - { - var count = Count; - _suspendCount = true; - return Disposable.Create(() => - { - _suspendCount = false; - - if (Count != count) - { - OnPropertyChanged(new PropertyChangedEventArgs("Count")); - } - }); - } - - /// - /// Raises the event. + /// Adds the elements of the specified collection to the end of the collection. /// - /// The instance containing the event data. - protected override void OnPropertyChanged(PropertyChangedEventArgs e) + /// The collection whose elements should be added to the end of the List. The collection itself cannot be null, but it can contain elements that are null. + /// is null. + public void AddRange(IEnumerable collection) { - if (e == null) + if (collection is null) { - throw new ArgumentNullException(nameof(e)); + throw new ArgumentNullException(nameof(collection)); } - if (_suspendCount && e.PropertyName == "Count") + foreach (var item in collection) { - return; + Add(item); } - - base.OnPropertyChanged(e); } /// - /// Raises the event. + /// Inserts the elements of a collection into the at the specified index. /// - /// The instance containing the event data. - protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + /// Inserts the items at the specified index. + /// The zero-based index at which the new elements should be inserted. + /// is null. + /// is less than 0.-or- is greater than Count. + public void InsertRange(IEnumerable collection, int index) { - if (_suspendNotifications) + if (collection is null) { - return; + throw new ArgumentNullException(nameof(collection)); } - base.OnCollectionChanged(e); + foreach (var item in collection) + { + InsertItem(index++, item); + } } - #endregion - /// /// Clears the list and Loads the specified items. /// /// The items. public void Load(IEnumerable items) { - if (items == null) + if (items is null) { throw new ArgumentNullException(nameof(items)); } @@ -143,57 +104,88 @@ public void Load(IEnumerable items) } } - #region Implementation of IExtendedList - /// - /// Adds the elements of the specified collection to the end of the collection. + /// Removes a range of elements from the . /// - /// The collection whose elements should be added to the end of the List. The collection itself cannot be null, but it can contain elements that are null - /// is null. - public void AddRange(IEnumerable collection) + /// The zero-based starting index of the range of elements to remove.The number of elements to remove. is less than 0.-or- is less than 0. and do not denote a valid range of elements in the . + public void RemoveRange(int index, int count) { - if (collection == null) + for (var i = 0; i < count; i++) { - throw new ArgumentNullException(nameof(collection)); + RemoveAt(index); } + } - foreach (var item in collection) - { - Add(item); - } + /// + /// Suspends count notifications. + /// + /// A disposable when disposed will reset the count. + public IDisposable SuspendCount() + { + var count = Count; + _suspendCount = true; + return Disposable.Create( + () => + { + _suspendCount = false; + + if (Count != count) + { + OnPropertyChanged(new PropertyChangedEventArgs("Count")); + } + }); } /// - /// Inserts the elements of a collection into the at the specified index. + /// Suspends notifications. When disposed, a reset notification is fired. /// - /// Inserts the items at the specified index - /// The zero-based index at which the new elements should be inserted. - /// is null. - /// is less than 0.-or- is greater than . - public void InsertRange(IEnumerable collection, int index) + /// A disposable when disposed will reset notifications. + public IDisposable SuspendNotifications() { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } + _suspendCount = true; + _suspendNotifications = true; - foreach (var item in collection) + return Disposable.Create( + () => + { + _suspendCount = false; + _suspendNotifications = false; + OnPropertyChanged(new PropertyChangedEventArgs("Count")); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + }); + } + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (_suspendNotifications) { - InsertItem(index++, item); + return; } + + base.OnCollectionChanged(e); } /// - /// Removes a range of elements from the . + /// Raises the event. /// - /// The zero-based starting index of the range of elements to remove.The number of elements to remove. is less than 0.-or- is less than 0. and do not denote a valid range of elements in the . - public void RemoveRange(int index, int count) + /// The instance containing the event data. + protected override void OnPropertyChanged(PropertyChangedEventArgs e) { - for (var i = 0; i < count; i++) + if (e is null) { - RemoveAt(index); + throw new ArgumentNullException(nameof(e)); } + + if (_suspendCount && e.PropertyName == "Count") + { + return; + } + + base.OnPropertyChanged(e); } - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/ObservablePropertyFactory.cs b/src/DynamicData/Binding/ObservablePropertyFactory.cs index cdb76416d..c247a03fa 100644 --- a/src/DynamicData/Binding/ObservablePropertyFactory.cs +++ b/src/DynamicData/Binding/ObservablePropertyFactory.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -12,62 +12,47 @@ namespace DynamicData.Binding { - - internal static class Observable - { - public static readonly IObservable Empty = Observable.Empty(); - public static readonly IObservable Never = Observable.Never(); - public static readonly IObservable Default = Observable.Return(default(T)); - } - internal class ObservablePropertyFactory - where TObject: INotifyPropertyChanged + where TObject : INotifyPropertyChanged { private readonly Func>> _factory; public ObservablePropertyFactory(Func valueAccessor, ObservablePropertyPart[] chain) { _factory = (t, notifyInitial) => - { - //1) notify when values have changed - //2) resubscribe when changed because it may be a child object which has changed - var valueHasChanged = GetNotifiers(t,chain).Merge().Take(1).Repeat(); - if (notifyInitial) { - valueHasChanged = Observable.Defer(() => Observable.Return(Unit.Default)) - .Concat(valueHasChanged); - } - - return valueHasChanged.Select(_ => GetPropertyValue(t,chain, valueAccessor)); - }; + // 1) notify when values have changed + // 2) resubscribe when changed because it may be a child object which has changed + var valueHasChanged = GetNotifiers(t, chain).Merge().Take(1).Repeat(); + if (notifyInitial) + { + valueHasChanged = Observable.Defer(() => Observable.Return(Unit.Default)).Concat(valueHasChanged); + } + + return valueHasChanged.Select(_ => GetPropertyValue(t, chain, valueAccessor)); + }; } public ObservablePropertyFactory(Expression> expression) { - //this overload is used for shallow observations i.e. depth = 1, so no need for re-subscriptions + // this overload is used for shallow observations i.e. depth = 1, so no need for re-subscriptions var member = expression.GetProperty(); var accessor = expression.Compile(); _factory = (t, notifyInitial) => - { - PropertyValue Factory() => new PropertyValue(t, accessor(t)); + { + PropertyValue Factory() => new(t, accessor(t)); - var propertyChanged = Observable.FromEventPattern - ( - handler => t.PropertyChanged += handler, - handler => t.PropertyChanged -= handler - ) - .Where(args => args.EventArgs.PropertyName == member.Name) - .Select(x => Factory()); + var propertyChanged = Observable.FromEventPattern(handler => t.PropertyChanged += handler, handler => t.PropertyChanged -= handler).Where(args => args.EventArgs.PropertyName == member.Name).Select(_ => Factory()); - if (!notifyInitial) - { - return propertyChanged; - } + if (!notifyInitial) + { + return propertyChanged; + } - var initial = Observable.Defer(() => Observable.Return(Factory())); - return initial.Concat(propertyChanged); - }; + var initial = Observable.Defer(() => Observable.Return(Factory())); + return initial.Concat(propertyChanged); + }; } public IObservable> Create(TObject source, bool notifyInitial) @@ -75,8 +60,8 @@ public IObservable> Create(TObject source, boo return _factory(source, notifyInitial); } - //create notifier for all parts of the property path - private static IEnumerable> GetNotifiers(TObject source, ObservablePropertyPart[] chain) + // create notifier for all parts of the property path + private static IEnumerable> GetNotifiers(TObject source, IEnumerable chain) { object value = source; foreach (var metadata in chain.Reverse()) @@ -85,21 +70,21 @@ private static IEnumerable> GetNotifiers(TObject source, Obser value = metadata.Accessor(value); yield return obs; - if (value == null) + if (value is null) { yield break; } } } - //walk the tree and break at a null, or return the value [should reduce this to a null an expression] - private static PropertyValue GetPropertyValue(TObject source, ObservablePropertyPart[] chain, Func valueAccessor) + // walk the tree and break at a null, or return the value [should reduce this to a null an expression] + private static PropertyValue GetPropertyValue(TObject source, IEnumerable chain, Func valueAccessor) { object value = source; foreach (var metadata in chain.Reverse()) { value = metadata.Accessor(value); - if (value == null) + if (value is null) { return new PropertyValue(source); } @@ -107,6 +92,5 @@ private static PropertyValue GetPropertyValue(TObject source return new PropertyValue(source, valueAccessor(source)); } - } } \ No newline at end of file diff --git a/src/DynamicData/Binding/ObservablePropertyFactoryCache.cs b/src/DynamicData/Binding/ObservablePropertyFactoryCache.cs index 31dc242ca..946e6f02f 100644 --- a/src/DynamicData/Binding/ObservablePropertyFactoryCache.cs +++ b/src/DynamicData/Binding/ObservablePropertyFactoryCache.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -12,9 +12,9 @@ namespace DynamicData.Binding { internal sealed class ObservablePropertyFactoryCache { - private readonly ConcurrentDictionary _factories = new ConcurrentDictionary(); + public static readonly ObservablePropertyFactoryCache Instance = new(); - public static readonly ObservablePropertyFactoryCache Instance = new ObservablePropertyFactoryCache(); + private readonly ConcurrentDictionary _factories = new(); private ObservablePropertyFactoryCache() { @@ -25,24 +25,26 @@ public ObservablePropertyFactory GetFactory - { - ObservablePropertyFactory factory; - - var memberChain = expression.GetMemberChain().ToArray(); - if (memberChain.Length == 1) - { - factory = new ObservablePropertyFactory(expression); - } - else - { - var chain = memberChain.Select(m => new ObservablePropertyPart(m)).ToArray(); - var accessor = expression?.Compile() ?? throw new ArgumentNullException(nameof(expression)); - factory = new ObservablePropertyFactory(accessor, chain); - } - - return factory; - }); + var result = _factories.GetOrAdd( + key, + _ => + { + ObservablePropertyFactory factory; + + var memberChain = expression.GetMemberChain().ToArray(); + if (memberChain.Length == 1) + { + factory = new ObservablePropertyFactory(expression); + } + else + { + var chain = memberChain.Select(m => new ObservablePropertyPart(m)).ToArray(); + var accessor = expression.Compile() ?? throw new ArgumentNullException(nameof(expression)); + factory = new ObservablePropertyFactory(accessor, chain); + } + + return factory; + }); return (ObservablePropertyFactory)result; } diff --git a/src/DynamicData/Binding/ObservablePropertyPart.cs b/src/DynamicData/Binding/ObservablePropertyPart.cs index 912d2b992..919412d95 100644 --- a/src/DynamicData/Binding/ObservablePropertyPart.cs +++ b/src/DynamicData/Binding/ObservablePropertyPart.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,13 +9,11 @@ namespace DynamicData.Binding { - [DebuggerDisplay("ObservablePropertyPart<{_expression}>")] + [DebuggerDisplay("ObservablePropertyPart<{" + nameof(_expression) + "}>")] internal sealed class ObservablePropertyPart { // ReSharper disable once NotAccessedField.Local private readonly MemberExpression _expression; - public Func> Factory { get; } - public Func Accessor { get; } public ObservablePropertyPart(MemberExpression expression) { @@ -23,5 +21,9 @@ public ObservablePropertyPart(MemberExpression expression) Factory = expression.CreatePropertyChangedFactory(); Accessor = expression.CreateValueAccessor(); } + + public Func Accessor { get; } + + public Func> Factory { get; } } } \ No newline at end of file diff --git a/src/DynamicData/Binding/PropertyValue.cs b/src/DynamicData/Binding/PropertyValue.cs index 783e5cd7f..32b1e7a5b 100644 --- a/src/DynamicData/Binding/PropertyValue.cs +++ b/src/DynamicData/Binding/PropertyValue.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,7 +8,7 @@ namespace DynamicData.Binding { /// - /// Container holding sender and latest property value + /// Container holding sender and latest property value. /// /// The type of the object. /// The type of the value. @@ -29,28 +29,52 @@ internal PropertyValue(TObject sender) { Sender = sender; UnobtainableValue = true; - Value = default(TValue); + Value = default; } /// - /// The Sender + /// Gets the Sender. /// public TObject Sender { get; } /// - /// Latest observed value + /// Gets latest observed value. /// - public TValue Value { get; } + public TValue? Value { get; } /// - /// Flag to indicated that the value was unobtainable when observing a deeply nested struct + /// Gets a value indicating whether flag to indicated that the value was unobtainable when observing a deeply nested struct. /// internal bool UnobtainableValue { get; } - #region Equality + /// + /// Implements the operator ==. + /// + /// The left. + /// The right. + /// + /// The result of the operator. + /// + public static bool operator ==(PropertyValue? left, PropertyValue? right) + { + return Equals(left, right); + } + + /// + /// Implements the operator !=. + /// + /// The left. + /// The right. + /// + /// The result of the operator. + /// + public static bool operator !=(PropertyValue? left, PropertyValue? right) + { + return !Equals(left, right); + } /// - public bool Equals(PropertyValue other) + public bool Equals(PropertyValue? other) { if (ReferenceEquals(null, other)) { @@ -62,76 +86,38 @@ public bool Equals(PropertyValue other) return true; } - return EqualityComparer.Default.Equals(Sender, other.Sender) && EqualityComparer.Default.Equals(Value, other.Value); - } - - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) + if (other.Value is null && Value is null) { - return false; + return true; } - if (ReferenceEquals(this, obj)) + if (other.Value is null || Value is null) { - return true; + return false; } - return obj is PropertyValue && Equals((PropertyValue)obj); + return EqualityComparer.Default.Equals(Sender, other.Sender) && EqualityComparer.Default.Equals(Value, other.Value); } - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// + /// + public override bool Equals(object? obj) + { + return obj is PropertyValue propertyValue && Equals(propertyValue); + } + + /// public override int GetHashCode() { unchecked { - return (EqualityComparer.Default.GetHashCode(Sender) * 397) ^ EqualityComparer.Default.GetHashCode(Value); + return (Sender is null ? 0 : EqualityComparer.Default.GetHashCode(Sender) * 397) ^ (Value is null ? 0 : EqualityComparer.Default.GetHashCode(Value)); } } - /// - /// Implements the operator ==. - /// - /// The left. - /// The right. - /// - /// The result of the operator. - /// - public static bool operator ==(PropertyValue left, PropertyValue right) - { - return Equals(left, right); - } - - /// - /// Implements the operator !=. - /// - /// The left. - /// The right. - /// - /// The result of the operator. - /// - public static bool operator !=(PropertyValue left, PropertyValue right) - { - return !Equals(left, right); - } - - #endregion - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// + /// public override string ToString() { return $"{Sender} ({Value})"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/SortDirection.cs b/src/DynamicData/Binding/SortDirection.cs index c6c0fa4f8..70ef890f2 100644 --- a/src/DynamicData/Binding/SortDirection.cs +++ b/src/DynamicData/Binding/SortDirection.cs @@ -1,22 +1,22 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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 { /// - /// Sort direction + /// Sort direction. /// public enum SortDirection { /// - /// Sort items ascending + /// Sort items ascending. /// Ascending, /// - /// Sort items descending + /// Sort items descending. /// Descending } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/SortExpression.cs b/src/DynamicData/Binding/SortExpression.cs index 91eb4a918..ad7c30c49 100644 --- a/src/DynamicData/Binding/SortExpression.cs +++ b/src/DynamicData/Binding/SortExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,9 +7,9 @@ namespace DynamicData.Binding { /// - /// A value expression with sort direction + /// A value expression with sort direction. /// - /// + /// The type of the item. public class SortExpression { /// @@ -24,13 +24,13 @@ public SortExpression(Func expression, SortDirection direction = } /// - /// Gets or sets the direction. + /// Gets the direction. /// public SortDirection Direction { get; } /// - /// Gets or sets the expression. + /// Gets the expression. /// public Func Expression { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/SortExpressionComparer.cs b/src/DynamicData/Binding/SortExpressionComparer.cs index 2ba55dee5..2261db877 100644 --- a/src/DynamicData/Binding/SortExpressionComparer.cs +++ b/src/DynamicData/Binding/SortExpressionComparer.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,32 +8,47 @@ namespace DynamicData.Binding { /// - /// Generic sort expression to help create inline sorting for the .Sort(IComparer comparer) operator + /// Generic sort expression to help create inline sorting for the .Sort(IComparer comparer) operator. /// - /// + /// The item to sort against. public class SortExpressionComparer : List>, IComparer { /// - /// Compares x and y + /// Create an ascending sort expression. /// - /// The x. - /// The y. - /// - public int Compare(T x, T y) + /// The expression. + /// A comparer in ascending order. + public static SortExpressionComparer Ascending(Func expression) + { + return new SortExpressionComparer { new(expression) }; + } + + /// + /// Create an descending sort expression. + /// + /// The expression. + /// A comparer in descending order. + public static SortExpressionComparer Descending(Func expression) + { + return new SortExpressionComparer { new(expression, SortDirection.Descending) }; + } + + /// + public int Compare(T? x, T? y) { foreach (var item in this) { - if (x == null && y == null) + if (x is null && y is null) { continue; } - if (x == null) + if (x is null) { return -1; } - if (y == null) + if (y is null) { return 1; } @@ -41,17 +56,17 @@ public int Compare(T x, T y) var xValue = item.Expression(x); var yValue = item.Expression(y); - if (xValue == null && yValue == null) + if (xValue is null && yValue is null) { continue; } - if (xValue == null) + if (xValue is null) { return -1; } - if (yValue == null) + if (yValue is null) { return 1; } @@ -69,30 +84,10 @@ public int Compare(T x, T y) } /// - /// Create an ascending sort expression - /// - /// The expression. - /// - public static SortExpressionComparer Ascending(Func expression) - { - return new SortExpressionComparer { new SortExpression(expression) }; - } - - /// - /// Create an descending sort expression. - /// - /// The expression. - /// - public static SortExpressionComparer Descending(Func expression) - { - return new SortExpressionComparer { new SortExpression(expression, SortDirection.Descending) }; - } - - /// - /// Adds an additional ascending sort expression + /// Adds an additional ascending sort expression. /// /// The expression. - /// + /// A comparer in ascending order first taking into account the comparer passed in. public SortExpressionComparer ThenByAscending(Func expression) { Add(new SortExpression(expression)); @@ -100,14 +95,14 @@ public SortExpressionComparer ThenByAscending(Func expression } /// - /// Adds an additional descending sort expression + /// Adds an additional descending sort expression. /// /// The expression. - /// + /// A comparer in descending order first taking into account the comparer passed in. public SortExpressionComparer ThenByDescending(Func expression) { Add(new SortExpression(expression, SortDirection.Descending)); return this; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Binding/SortedBindingListAdaptor.cs b/src/DynamicData/Binding/SortedBindingListAdaptor.cs index accf2f318..38e194e3a 100644 --- a/src/DynamicData/Binding/SortedBindingListAdaptor.cs +++ b/src/DynamicData/Binding/SortedBindingListAdaptor.cs @@ -1,27 +1,33 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. #if SUPPORTS_BINDINGLIST - using System; using System.ComponentModel; using System.Linq; -using DynamicData.Annotations; namespace DynamicData.Binding { /// /// Represents an adaptor which is used to update a binding list from - /// a sorted change set + /// a sorted change set. /// + /// The type of object. + /// The type of key. public class SortedBindingListAdaptor : ISortedChangeSetAdaptor + where TKey : notnull { private readonly BindingList _list; + private readonly int _refreshThreshold; - /// - public SortedBindingListAdaptor([NotNull] BindingList list, int refreshThreshold = 25) + /// + /// Initializes a new instance of the class. + /// + /// The source list. + /// The threshold before a refresh is triggered. + public SortedBindingListAdaptor(BindingList list, int refreshThreshold = 25) { _list = list ?? throw new ArgumentNullException(nameof(list)); _refreshThreshold = refreshThreshold; @@ -30,7 +36,7 @@ public SortedBindingListAdaptor([NotNull] BindingList list, int refresh /// public void Adapt(ISortedChangeSet changes) { - if (changes == null) + if (changes is null) { throw new ArgumentNullException(nameof(changes)); } @@ -82,13 +88,16 @@ private void DoUpdate(ISortedChangeSet changes) case ChangeReason.Add: _list.Insert(change.CurrentIndex, change.Current); break; + case ChangeReason.Remove: _list.RemoveAt(change.CurrentIndex); break; + case ChangeReason.Moved: _list.RemoveAt(change.PreviousIndex); _list.Insert(change.CurrentIndex, change.Current); break; + case ChangeReason.Update: _list.RemoveAt(change.PreviousIndex); _list.Insert(change.CurrentIndex, change.Current); @@ -98,4 +107,5 @@ private void DoUpdate(ISortedChangeSet changes) } } } + #endif \ No newline at end of file diff --git a/src/DynamicData/Binding/SortedObservableCollectionAdaptor.cs b/src/DynamicData/Binding/SortedObservableCollectionAdaptor.cs index 9a44ca0cf..b6176a159 100644 --- a/src/DynamicData/Binding/SortedObservableCollectionAdaptor.cs +++ b/src/DynamicData/Binding/SortedObservableCollectionAdaptor.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,36 +9,37 @@ namespace DynamicData.Binding { /// /// Represents an adaptor which is used to update observable collection from - /// a sorted change set stream + /// a sorted change set stream. /// /// The type of the object. /// The type of the key. public class SortedObservableCollectionAdaptor : ISortedObservableCollectionAdaptor + where TKey : notnull { private readonly int _refreshThreshold; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The number of changes before a Reset event is used + /// The number of changes before a Reset event is used. public SortedObservableCollectionAdaptor(int refreshThreshold = 25) { _refreshThreshold = refreshThreshold; } /// - /// Maintains the specified collection from the changes + /// Maintains the specified collection from the changes. /// /// The changes. /// The collection. public void Adapt(ISortedChangeSet changes, IObservableCollection collection) { - if (changes == null) + if (changes is null) { throw new ArgumentNullException(nameof(changes)); } - if (collection == null) + if (collection is null) { throw new ArgumentNullException(nameof(collection)); } @@ -56,7 +57,7 @@ public void Adapt(ISortedChangeSet changes, IObservableCollection break; case SortReason.DataChanged: - if (changes.Count - changes.Refreshes > _refreshThreshold) + if (changes.Count - changes.Refreshes > _refreshThreshold) { using (collection.SuspendNotifications()) { @@ -74,7 +75,7 @@ public void Adapt(ISortedChangeSet changes, IObservableCollection break; case SortReason.Reorder: - //Updates will only be moves, so apply logic + // Updates will only be moves, so apply logic using (collection.SuspendCount()) { DoUpdate(changes, collection); @@ -96,12 +97,15 @@ private static void DoUpdate(ISortedChangeSet updates, IObservabl case ChangeReason.Add: list.Insert(update.CurrentIndex, update.Current); break; + case ChangeReason.Remove: list.RemoveAt(update.CurrentIndex); break; + case ChangeReason.Moved: list.Move(update.PreviousIndex, update.CurrentIndex); break; + case ChangeReason.Update: if (update.PreviousIndex != update.CurrentIndex) { @@ -118,4 +122,4 @@ private static void DoUpdate(ISortedChangeSet updates, IObservabl } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/CacheChangeSetEx.cs b/src/DynamicData/Cache/CacheChangeSetEx.cs new file mode 100644 index 000000000..fc169f6f5 --- /dev/null +++ b/src/DynamicData/Cache/CacheChangeSetEx.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2011-2020 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.Cache +{ + internal static class CacheChangeSetEx + { + /// + /// IChangeSet is flawed because it automatically means allocations when enumerating. + /// This extension is a crazy hack to cast to the concrete change set which means we no longer allocate + /// as change set now inherits from List which has allocation free enumerations. + /// + /// IChangeSet will be removed in V7 and instead Change sets will be used directly + /// + /// In the mean time I am banking that no-one has implemented a custom change set - personally I think it is very unlikely. + /// + /// The source change set. + public static ChangeSet ToConcreteType(this IChangeSet changeSet) + where TKey : notnull + => (ChangeSet)changeSet; + } +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Change.cs b/src/DynamicData/Cache/Change.cs index b7eb081c3..7f3b47b05 100644 --- a/src/DynamicData/Cache/Change.cs +++ b/src/DynamicData/Cache/Change.cs @@ -1,53 +1,23 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Container to describe a single change to a cache + /// Container to describe a single change to a cache. /// + /// The type of the object. + /// The type of the key. public readonly struct Change : IEquatable> + where TKey : notnull { - /// - /// The unique key of the item which has changed - /// - public TKey Key { get; } - - /// - /// The reason for the change - /// - public ChangeReason Reason { get; } - - /// - /// The item which has changed - /// - public TObject Current { get; } - - /// - /// The current index - /// - public int CurrentIndex { get; } - - /// - /// The previous change. - /// - /// This is only when Reason==ChangeReason.Replace. - /// - public Optional Previous { get; } - - /// - /// The previous change. - /// - /// This is only when Reason==ChangeReason.Update or ChangeReason.Move. - /// - public int PreviousIndex { get; } - /// /// Initializes a new instance of the struct. /// @@ -61,7 +31,8 @@ public Change(ChangeReason reason, TKey key, TObject current, int index = -1) } /// - /// Constructor for ChangeReason.Move + /// Initializes a new instance of the struct. + /// Constructor for ChangeReason.Move. /// /// The key. /// The current. @@ -70,7 +41,7 @@ public Change(ChangeReason reason, TKey key, TObject current, int index = -1) /// /// CurrentIndex must be greater than or equal to zero /// or - /// PreviousIndex must be greater than or equal to zero + /// PreviousIndex must be greater than or equal to zero. /// public Change(TKey key, TObject current, int currentIndex, int previousIndex) : this() @@ -105,7 +76,7 @@ public Change(TKey key, TObject current, int currentIndex, int previousIndex) /// /// For ChangeReason.Add, a previous value cannot be specified /// or - /// For ChangeReason.Change, must supply previous value + /// For ChangeReason.Change, must supply previous value. /// public Change(ChangeReason reason, TKey key, TObject current, Optional previous, int currentIndex = -1, int previousIndex = -1) : this() @@ -128,19 +99,57 @@ public Change(ChangeReason reason, TKey key, TObject current, Optional } } - #region Equality + /// + /// Gets the unique key of the item which has changed. + /// + public TKey Key { get; } + + /// + /// Gets the reason for the change. + /// + public ChangeReason Reason { get; } /// - /// Determines whether the specified objects are equal + /// Gets the item which has changed. /// + public TObject Current { get; } + + /// + /// Gets the current index. + /// + public int CurrentIndex { get; } + + /// + /// Gets the previous change. + /// + /// This is only when Reason==ChangeReason.Replace. + /// + public Optional Previous { get; } + + /// + /// Gets the previous change. + /// + /// This is only when Reason==ChangeReason.Update or ChangeReason.Move. + /// + public int PreviousIndex { get; } + + /// + /// Determines whether the specified objects are equal. + /// + /// The left value to compare. + /// The right value to compare. + /// If the two values are equal to each other. public static bool operator ==(Change left, Change right) { return left.Equals(right); } /// - /// Determines whether the specified objects are equal + /// Determines whether the specified objects are equal. /// + /// The left value to compare. + /// The right value to compare. + /// If the two values are not equal to each other. public static bool operator !=(Change left, Change right) { return !left.Equals(right); @@ -149,23 +158,18 @@ public Change(ChangeReason reason, TKey key, TObject current, Optional /// public bool Equals(Change other) { - return EqualityComparer.Default.Equals(Key, other.Key) - && Reason == other.Reason - && EqualityComparer.Default.Equals(Current, other.Current) - && CurrentIndex == other.CurrentIndex - && Previous.Equals(other.Previous) - && PreviousIndex == other.PreviousIndex; + return EqualityComparer.Default.Equals(Key, other.Key) && Reason == other.Reason && EqualityComparer.Default.Equals(Current, other.Current) && CurrentIndex == other.CurrentIndex && Previous.Equals(other.Previous) && PreviousIndex == other.PreviousIndex; } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { return false; } - return obj is Change && Equals((Change) obj); + return obj is Change change && Equals(change); } /// @@ -174,8 +178,8 @@ public override int GetHashCode() unchecked { var hashCode = EqualityComparer.Default.GetHashCode(Key); - hashCode = (hashCode * 397) ^ (int) Reason; - hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Current); + hashCode = (hashCode * 397) ^ (int)Reason; + hashCode = (hashCode * 397) ^ (Current is null ? 0 : EqualityComparer.Default.GetHashCode(Current)); hashCode = (hashCode * 397) ^ CurrentIndex; hashCode = (hashCode * 397) ^ Previous.GetHashCode(); hashCode = (hashCode * 397) ^ PreviousIndex; @@ -183,12 +187,10 @@ public override int GetHashCode() } } - #endregion - /// public override string ToString() { return $"{Reason}, Key: {Key}, Current: {Current}, Previous: {Previous}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ChangeAwareCache.cs b/src/DynamicData/Cache/ChangeAwareCache.cs index 752d055f2..2c7f1f853 100644 --- a/src/DynamicData/Cache/ChangeAwareCache.cs +++ b/src/DynamicData/Cache/ChangeAwareCache.cs @@ -1,10 +1,12 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Linq; + +using DynamicData.Cache; using DynamicData.Kernel; // ReSharper disable once CheckNamespace @@ -14,51 +16,63 @@ namespace DynamicData /// A cache which captures all changes which are made to it. These changes are recorded until CaptureChanges() at which point thw changes are cleared. /// Used for creating custom operators. /// - /// - public sealed class ChangeAwareCache : ICache + /// + /// The value of the cache. + /// The key of the cache. + public sealed class ChangeAwareCache : ICache + where TKey : notnull { + private readonly Dictionary _data; private ChangeSet _changes; - private Dictionary _data; - - /// - public int Count => _data?.Count ?? 0; - - /// - public IEnumerable> KeyValues => _data ?? Enumerable.Empty>(); - - /// - public IEnumerable Items => _data?.Values ?? Enumerable.Empty(); - - /// - public IEnumerable Keys => _data?.Keys ?? Enumerable.Empty(); - - /// + /// + /// Initializes a new instance of the class. + /// public ChangeAwareCache() { + _changes = new ChangeSet(); + _data = new Dictionary(); } - /// + /// + /// Initializes a new instance of the class. + /// + /// The capacity of the initial items. public ChangeAwareCache(int capacity) { - EnsureInitialised(capacity); + _changes = new ChangeSet(capacity); + _data = new Dictionary(capacity); } - /// Initializes a new instance of the class. + /// + /// Initializes a new instance of the class. + /// + /// Data to populate the cache with. public ChangeAwareCache(Dictionary data) { - _data = data; + _data = data ?? throw new ArgumentNullException(nameof(data)); + _changes = new ChangeSet(); } /// - public Optional Lookup(TKey key) => _data?.Lookup(key) ?? Optional.None; + public int Count => _data.Count; + + /// + public IEnumerable Items => _data.Values; + + /// + public IEnumerable Keys => _data.Keys; + + /// + public IEnumerable> KeyValues => _data; /// - /// Adds the item to the cache without checking whether there is an existing value in the cache + /// Adds the item to the cache without checking whether there is an existing value in the cache. /// + /// The item to add. + /// The key to add. public void Add(TObject item, TKey key) { - EnsureInitialised(); _changes.Add(new Change(ChangeReason.Add, key, item)); _data.Add(key, item); } @@ -66,88 +80,90 @@ public void Add(TObject item, TKey key) /// public void AddOrUpdate(TObject item, TKey key) { - EnsureInitialised(); - - _changes.Add(_data.TryGetValue(key, out var existingItem) - ? new Change(ChangeReason.Update, key, item, existingItem) - : new Change(ChangeReason.Add, key, item)); + _changes.Add(_data.TryGetValue(key, out var existingItem) ? new Change(ChangeReason.Update, key, item, existingItem) : new Change(ChangeReason.Add, key, item)); _data[key] = item; } /// - /// Removes the item matching the specified keys. + /// Create a change set from recorded changes and clears known changes. /// - /// The keys. - public void Remove(IEnumerable keys) + /// A change set with the key/value changes. + public ChangeSet CaptureChanges() { - if (keys == null) + if (_changes.Count == 0) { - throw new ArgumentNullException(nameof(keys)); + return ChangeSet.Empty; } - if (_data == null) - { - return; - } + var copy = _changes; + _changes = new ChangeSet(); + return copy; + } - if (keys is IList list) - { - EnsureInitialised(list.Count); - var enumerable = EnumerableIList.Create(list); - foreach (var item in enumerable) - { - Remove(item); - } - } - else - { - EnsureInitialised(); - foreach (var key in keys) - { - Remove(key); - } - } + /// + public void Clear() + { + var toRemove = _data.Select(kvp => new Change(ChangeReason.Remove, kvp.Key, kvp.Value)); + _changes.AddRange(toRemove); + _data.Clear(); } /// - public void Remove(TKey key) + public void Clone(IChangeSet changes) { - if (_data == null) + if (changes is null) { - return; + throw new ArgumentNullException(nameof(changes)); } - if (_data.TryGetValue(key, out var existingItem)) + foreach (var change in changes.ToConcreteType()) { - EnsureInitialised(); - _changes.Add(new Change(ChangeReason.Remove, key, existingItem)); - _data.Remove(key); + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + AddOrUpdate(change.Current, change.Key); + break; + + case ChangeReason.Remove: + Remove(change.Key); + break; + + case ChangeReason.Refresh: + Refresh(change.Key); + break; + case ChangeReason.Moved: + break; + default: + throw new ArgumentOutOfRangeException(nameof(changes)); + } } } + /// + public Optional Lookup(TKey key) => _data.Lookup(key); + /// - /// Raises an evaluate change for the specified keys + /// Raises an evaluate change for the specified keys. /// + /// The keys to refresh. public void Refresh(IEnumerable keys) { - if (keys == null) + if (keys is null) { throw new ArgumentNullException(nameof(keys)); } if (keys is IList list) { - EnsureInitialised(list.Count); - var enumerable = EnumerableIList.Create(list); - foreach (var key in enumerable) + foreach (var key in EnumerableIList.Create(list)) { Refresh(key); } } else { - EnsureInitialised(); foreach (var key in keys) { Refresh(key); @@ -156,98 +172,60 @@ public void Refresh(IEnumerable keys) } /// - /// Raises an evaluate change for all items in the cache + /// Raises an evaluate change for all items in the cache. /// public void Refresh() { - EnsureInitialised(_data.Count); _changes.AddRange(_data.Select(t => new Change(ChangeReason.Refresh, t.Key, t.Value))); } /// - /// Raises an evaluate change for the specified key + /// Raises an evaluate change for the specified key. /// /// The key. public void Refresh(TKey key) { - EnsureInitialised(); if (_data.TryGetValue(key, out var existingItem)) { _changes.Add(new Change(ChangeReason.Refresh, key, existingItem)); } } - /// - public void Clear() - { - if (_data == null) - { - return; - } - - EnsureInitialised(_data.Count); - - var toremove = _data.Select(kvp => new Change(ChangeReason.Remove, kvp.Key, kvp.Value)); - _changes.AddRange(toremove); - _data.Clear(); - } - - /// - public void Clone(IChangeSet changes) + /// + /// Removes the item matching the specified keys. + /// + /// The keys. + public void Remove(IEnumerable keys) { - if (changes == null) + if (keys is null) { - throw new ArgumentNullException(nameof(changes)); + throw new ArgumentNullException(nameof(keys)); } - EnsureInitialised(changes.Count); - - var enumerable = changes.ToConcreteType(); - foreach (var change in enumerable) + if (keys is IList list) { - switch (change.Reason) + foreach (var item in EnumerableIList.Create(list)) { - case ChangeReason.Add: - case ChangeReason.Update: - AddOrUpdate(change.Current, change.Key); - break; - case ChangeReason.Remove: - Remove(change.Key); - break; - case ChangeReason.Refresh: - Refresh(change.Key); - break; + Remove(item); } } - } - - private void EnsureInitialised(int capacity = -1) - { - if (_changes == null) - { - _changes = capacity > 0 ? new ChangeSet(capacity) : new ChangeSet(); - } - - if (_data == null) + else { - _data = capacity > 0 ? new Dictionary(capacity) : new Dictionary(); + foreach (var key in keys) + { + Remove(key); + } } } - /// - /// Create a changeset from recorded changes and clears known changes. - /// - public ChangeSet CaptureChanges() + /// + public void Remove(TKey key) { - if (_changes == null || _changes.Count==0) + if (_data.TryGetValue(key, out var existingItem)) { - return ChangeSet.Empty; + _changes.Add(new Change(ChangeReason.Remove, key, existingItem)); + _data.Remove(key); } - - var copy = _changes; - _changes = null; - return copy; } - } } \ No newline at end of file diff --git a/src/DynamicData/Cache/ChangeReason.cs b/src/DynamicData/Cache/ChangeReason.cs index c2e769a89..9b2b8f2f1 100644 --- a/src/DynamicData/Cache/ChangeReason.cs +++ b/src/DynamicData/Cache/ChangeReason.cs @@ -1,36 +1,40 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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. + +// ReSharper disable CheckNamespace namespace DynamicData { /// /// The reason for an individual change. /// - /// Used to signal consumers of any changes to the underlying cache + /// Used to signal consumers of any changes to the underlying cache. /// public enum ChangeReason { /// - /// An item has been added + /// An item has been added. /// Add = 0, /// - /// An item has been updated + /// An item has been updated. /// - Update =1, + Update = 1, /// - /// An item has removed + /// An item has removed. /// - Remove =2, + Remove = 2, /// - /// Downstream operators will refresh + /// Downstream operators will refresh. /// - Refresh =3, + Refresh = 3, /// - /// An item has been moved in a sorted collection + /// An item has been moved in a sorted collection. /// Moved = 4, } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ChangeSet.cs b/src/DynamicData/Cache/ChangeSet.cs index 344a09a57..155912165 100644 --- a/src/DynamicData/Cache/ChangeSet.cs +++ b/src/DynamicData/Cache/ChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,43 +8,41 @@ // ReSharper disable once CheckNamespace namespace DynamicData { - internal static class CacheChangeSetEx - { - /// - /// IChangeSet is flawed because it automatically means allocations when enumerating. - /// This extension is a crazy hack to cast to the concrete changeset which means we no longer allocate - /// as change set now inherits from List which has allocation free enumerations. - /// - /// IChangeSet will be removed in V7 and instead Change sets will be used directly - /// - /// In the mean time I am banking that no-one has implemented a custom change set - personally I think it is very unlikely - /// - public static ChangeSet ToConcreteType(this IChangeSet changeset) => (ChangeSet)changeset; - } - /// - /// A collection of changes + /// A collection of changes. /// + /// The type of the object. + /// The type of the key. public class ChangeSet : List>, IChangeSet + where TKey : notnull { /// - /// An empty change set + /// An empty change set. /// - public static readonly ChangeSet Empty = new ChangeSet(); + public static readonly ChangeSet Empty = new(); - /// + /// + /// Initializes a new instance of the class. + /// public ChangeSet() { } - /// + /// + /// Initializes a new instance of the class. + /// + /// The collection of items to start the change set with. public ChangeSet(IEnumerable> collection) : base(collection) { } - /// - public ChangeSet(int capacity) : base(capacity) + /// + /// Initializes a new instance of the class. + /// + /// The initial capacity of the change set. + public ChangeSet(int capacity) + : base(capacity) { } @@ -52,16 +50,16 @@ public ChangeSet(int capacity) : base(capacity) public int Adds => this.Count(c => c.Reason == ChangeReason.Add); /// - public int Updates => this.Count(c => c.Reason == ChangeReason.Update); + public int Moves => this.Count(c => c.Reason == ChangeReason.Moved); /// - public int Removes => this.Count(c => c.Reason == ChangeReason.Remove); + public int Refreshes => this.Count(c => c.Reason == ChangeReason.Refresh); /// - public int Refreshes => this.Count(c => c.Reason == ChangeReason.Refresh); + public int Removes => this.Count(c => c.Reason == ChangeReason.Remove); /// - public int Moves => this.Count(c => c.Reason == ChangeReason.Moved); + public int Updates => this.Count(c => c.Reason == ChangeReason.Update); /// public override string ToString() @@ -69,4 +67,4 @@ public override string ToString() return $"ChangeSet<{typeof(TObject).Name}.{typeof(TKey).Name}>. Count={Count}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/DistinctChangeSet.cs b/src/DynamicData/Cache/DistinctChangeSet.cs index 02e7ff3f0..e5d8f57ee 100644 --- a/src/DynamicData/Cache/DistinctChangeSet.cs +++ b/src/DynamicData/Cache/DistinctChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,6 +8,7 @@ namespace DynamicData { internal class DistinctChangeSet : ChangeSet, IDistinctChangeSet + where T : notnull { public DistinctChangeSet(IEnumerable> items) : base(items) @@ -18,8 +19,9 @@ public DistinctChangeSet() { } - public DistinctChangeSet(int capacity) : base(capacity) + public DistinctChangeSet(int capacity) + : base(capacity) { } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/GroupChangeSet.cs b/src/DynamicData/Cache/GroupChangeSet.cs index 71819e716..8da7198e8 100644 --- a/src/DynamicData/Cache/GroupChangeSet.cs +++ b/src/DynamicData/Cache/GroupChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,17 +8,18 @@ namespace DynamicData { internal sealed class GroupChangeSet : ChangeSet, TGroupKey>, IGroupChangeSet + where TKey : notnull + where TGroupKey : notnull { + public static readonly new IGroupChangeSet Empty = new GroupChangeSet(); - public new static readonly IGroupChangeSet Empty = new GroupChangeSet(); - - private GroupChangeSet() + public GroupChangeSet(IEnumerable, TGroupKey>> items) + : base(items) { } - public GroupChangeSet(IEnumerable, TGroupKey>> items) - : base(items) + private GroupChangeSet() { } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ICache.cs b/src/DynamicData/Cache/ICache.cs index ffaa74118..53f24e936 100644 --- a/src/DynamicData/Cache/ICache.cs +++ b/src/DynamicData/Cache/ICache.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,41 +10,31 @@ namespace DynamicData /// /// A cache which captures all changes which are made to it. These changes are recorded until CaptureChanges() at which point thw changes are cleared. /// - /// Used for creating custom operators + /// Used for creating custom operators. /// /// The type of the object. /// The type of the key. /// public interface ICache : IQuery + where TKey : notnull { /// - /// Clones the cache from the specified changes - /// - /// The changes. - void Clone(IChangeSet changes); - - /// - /// Adds or updates the item using the specified key + /// Adds or updates the item using the specified key. /// /// The item. /// The key. void AddOrUpdate(TObject item, TKey key); /// - /// Removes the item matching the specified key. - /// - /// The key. - void Remove(TKey key); - - /// - /// Removes all items matching the specified keys + /// Clears all items. /// - void Remove(IEnumerable keys); + void Clear(); /// - /// Clears all items + /// Clones the cache from the specified changes. /// - void Clear(); + /// The changes. + void Clone(IChangeSet changes); /// /// Sends a signal for operators to recalculate it's state. @@ -52,14 +42,27 @@ public interface ICache : IQuery void Refresh(); /// - /// Refreshes the items matching the specified keys + /// Refreshes the items matching the specified keys. /// /// The keys. void Refresh(IEnumerable keys); /// - /// Refreshes the item matching the specified key + /// Refreshes the item matching the specified key. /// + /// The key to refresh. void Refresh(TKey key); + + /// + /// Removes the item matching the specified key. + /// + /// The key. + void Remove(TKey key); + + /// + /// Removes all items matching the specified keys. + /// + /// The keys to remove. + void Remove(IEnumerable keys); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ICacheUpdater.cs b/src/DynamicData/Cache/ICacheUpdater.cs index b3f723094..701bf7536 100644 --- a/src/DynamicData/Cache/ICacheUpdater.cs +++ b/src/DynamicData/Cache/ICacheUpdater.cs @@ -1,56 +1,57 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + // ReSharper disable once CheckNamespace namespace DynamicData { /// /// Api for updating an intermediate cache /// - /// Use edit to produce singular changeset. + /// Use edit to produce singular change set. /// /// NB:The evaluate method is used to signal to any observing operators - /// to reevaluate whether the the object still matches downstream operators. + /// to reevaluate whether the object still matches downstream operators. /// This is primarily targeted to inline object changes such as datetime and calculated fields. /// /// /// The type of the object. /// The type of the key. public interface ICacheUpdater : IQuery + where TKey : notnull { /// /// Adds or updates the specified key value pairs. /// + /// The key value pairs to add or update. void AddOrUpdate(IEnumerable> keyValuePairs); /// /// Adds or updates the specified key value pair. /// + /// The key value pair to add or update. void AddOrUpdate(KeyValuePair item); /// /// Adds or updates the specified item / key pair. /// + /// The item to add or update. + /// The key to add or update. void AddOrUpdate(TObject item, TKey key); /// - /// Sends a signal for operators to recalculate it's state. - /// - void Refresh(); - - /// - /// Refreshes the items matching the specified keys. + /// Clears all items from the underlying cache. /// - /// The keys. - void Refresh(IEnumerable keys); + void Clear(); /// - /// Refreshes the item matching the specified key. + /// Clones the change set to the cache. /// - void Refresh(TKey key); + /// The changes to clone. + void Clone(IChangeSet changes); /// /// Sends a signal for operators to recalculate it's state. @@ -68,69 +69,82 @@ public interface ICacheUpdater : IQuery /// /// Refreshes the item matching the specified key. /// + /// The key to evaluate. [Obsolete(Constants.EvaluateIsDead)] void Evaluate(TKey key); /// - ///Removes the specified keys + /// Gets the key associated with the object. /// - void Remove(IEnumerable keys); + /// The item. + /// The key for the specified object. + TKey GetKey(TObject item); /// - /// Overload of remove due to ambiguous method when TObject and TKey are of the same type. + /// Gets the key values for the specified items. /// - /// The key. - void RemoveKeys(IEnumerable key); + /// The items. + /// An enumeration of key value pairs of the key and the object. + IEnumerable> GetKeyValues(IEnumerable items); /// - ///Remove the specified keys + /// Sends a signal for operators to recalculate it's state. /// - void Remove(TKey key); + void Refresh(); /// - /// Overload of remove due to ambiguous method when TObject and TKey are of the same type. + /// Refreshes the items matching the specified keys. /// - /// The key. - void RemoveKey(TKey key); + /// The keys. + void Refresh(IEnumerable keys); /// - /// Removes the specified key value pairs. + /// Refreshes the item matching the specified key. /// - void Remove(IEnumerable> items); + /// The key to refresh. + void Refresh(TKey key); /// - ///Removes the specified key value pair. + /// Removes the specified keys. /// - void Remove(KeyValuePair item); + /// The keys to remove the values for. + void Remove(IEnumerable keys); /// - /// Updates using changes using the specified changeset. + /// Remove the specified keys. /// - [Obsolete("Use Clone()")] - void Update(IChangeSet changes); + /// The key of the item to remove. + void Remove(TKey key); /// - /// Clones the change set to the cache. + /// Removes the specified key value pairs. /// - void Clone(IChangeSet changes); + /// The key value pairs to remove. + void Remove(IEnumerable> items); /// - /// Clears all items from the underlying cache. + /// Removes the specified key value pair. /// - void Clear(); + /// The key value pair to remove. + void Remove(KeyValuePair item); /// - /// Gets the key associated with the object. + /// Overload of remove due to ambiguous method when TObject and TKey are of the same type. /// - /// The item. - /// The key for the specified object. - TKey GetKey(TObject item); + /// The key. + void RemoveKey(TKey key); /// - /// Gets the key values for the specified items. + /// Overload of remove due to ambiguous method when TObject and TKey are of the same type. /// - /// The items. - /// An enumeration of key value pairs of the key and the object. - IEnumerable> GetKeyValues(IEnumerable items); + /// The key. + void RemoveKeys(IEnumerable key); + + /// + /// Updates using changes using the specified change set. + /// + /// The changes to update with. + [Obsolete("Use Clone()")] + void Update(IChangeSet changes); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IChangeSet.cs b/src/DynamicData/Cache/IChangeSet.cs index 9d91db6f8..119b1ac79 100644 --- a/src/DynamicData/Cache/IChangeSet.cs +++ b/src/DynamicData/Cache/IChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -15,10 +15,11 @@ namespace DynamicData /// The type of the object. /// The type of the key. public interface IChangeSet : IChangeSet, IEnumerable> + where TKey : notnull { /// - /// The number of updates + /// Gets the number of updates. /// int Updates { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IChangeSetAdaptor.cs b/src/DynamicData/Cache/IChangeSetAdaptor.cs index 18186bdf1..37c1fac9f 100644 --- a/src/DynamicData/Cache/IChangeSetAdaptor.cs +++ b/src/DynamicData/Cache/IChangeSetAdaptor.cs @@ -1,12 +1,17 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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. + +// ReSharper disable CheckNamespace namespace DynamicData { /// - /// A simple adaptor to inject side effects into a changeset observable + /// A simple adaptor to inject side effects into a change set observable. /// /// The type of the object. /// The type of the key. public interface IChangeSetAdaptor + where TKey : notnull { /// /// Adapts the specified change. @@ -14,4 +19,4 @@ public interface IChangeSetAdaptor /// The change. void Adapt(IChangeSet change); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IConnectableCache.cs b/src/DynamicData/Cache/IConnectableCache.cs new file mode 100644 index 000000000..b4c8134a1 --- /dev/null +++ b/src/DynamicData/Cache/IConnectableCache.cs @@ -0,0 +1,45 @@ +// Copyright (c) 2011-2020 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. + +using System; + +// ReSharper disable CheckNamespace +namespace DynamicData +{ + /// + /// A cache for observing and querying in memory data. + /// + /// The type of the object. + /// The type of the key. + public interface IConnectableCache + where TKey : notnull + { + /// + /// Gets a count changed observable starting with the current count. + /// + IObservable CountChanged { get; } + + /// + /// Returns a filtered stream of cache changes preceded with the initial filtered state. + /// + /// The result will be filtered using the specified predicate. + /// An observable that emits the change set. + IObservable> Connect(Func? predicate = null); + + /// + /// Returns a filtered stream of cache changes. + /// Unlike Connect(), the returned observable is not prepended with the caches initial items. + /// + /// The result will be filtered using the specified predicate. + /// An observable that emits the change set. + IObservable> Preview(Func? predicate = null); + + /// + /// Returns an observable of any changes which match the specified key. The sequence starts with the initial item in the cache (if there is one). + /// + /// The key. + /// An observable that emits the change set. + IObservable> Watch(TKey key); + } +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IDistinctChangeSet.cs b/src/DynamicData/Cache/IDistinctChangeSet.cs index 773fc386d..f2b98edfc 100644 --- a/src/DynamicData/Cache/IDistinctChangeSet.cs +++ b/src/DynamicData/Cache/IDistinctChangeSet.cs @@ -1,11 +1,16 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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. + +// ReSharper disable CheckNamespace namespace DynamicData { /// /// A collection of distinct value updates. /// - /// + /// The type of the item. public interface IDistinctChangeSet : IChangeSet + where T : notnull { } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IGroup.cs b/src/DynamicData/Cache/IGroup.cs index 964a752f9..5e319007c 100644 --- a/src/DynamicData/Cache/IGroup.cs +++ b/src/DynamicData/Cache/IGroup.cs @@ -1,20 +1,25 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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. + +// ReSharper disable CheckNamespace namespace DynamicData { /// - /// An update stream which has been grouped by a common key + /// An update stream which has been grouped by a common key. /// /// The type of the object. /// The type of the key. - /// The type of value used to group the original stream + /// The type of value used to group the original stream. public interface IGroup : IKey + where TKey : notnull { /// - /// The observable for the group + /// Gets the observable for the group. /// /// /// The observable. /// IObservableCache Cache { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IGroupChangeSet.cs b/src/DynamicData/Cache/IGroupChangeSet.cs index b7fa54914..2fea11738 100644 --- a/src/DynamicData/Cache/IGroupChangeSet.cs +++ b/src/DynamicData/Cache/IGroupChangeSet.cs @@ -1,14 +1,19 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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. + +// ReSharper disable CheckNamespace namespace DynamicData { /// - /// A grouped change set + /// A grouped change set. /// - /// The source object type + /// The source object type. /// The type of the key.s - /// The value on which the stream has been grouped - public interface IGroupChangeSet : - IChangeSet, TGroupKey> + /// The value on which the stream has been grouped. + public interface IGroupChangeSet : IChangeSet, TGroupKey> + where TKey : notnull + where TGroupKey : notnull { } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IGrouping.cs b/src/DynamicData/Cache/IGrouping.cs index 2948db89d..b604a32ea 100644 --- a/src/DynamicData/Cache/IGrouping.cs +++ b/src/DynamicData/Cache/IGrouping.cs @@ -1,15 +1,16 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; + using DynamicData.Kernel; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Represents a group which provides an update after any value within the group changes + /// Represents a group which provides an update after any value within the group changes. /// /// The type of the object. /// The type of the key. @@ -17,40 +18,41 @@ namespace DynamicData public interface IGrouping { /// - /// Gets the group key + /// Gets the count. /// - TGroupKey Key { get; } + int Count { get; } /// - /// Gets the keys. + /// Gets the items. /// - IEnumerable Keys { get; } + IEnumerable Items { get; } /// - /// Gets the items. + /// Gets the group key. /// - IEnumerable Items { get; } + TGroupKey Key { get; } /// - /// Gets the items together with their keys + /// Gets the keys. + /// + IEnumerable Keys { get; } + + /// + /// Gets the items together with their keys. /// /// /// The key values. /// IEnumerable> KeyValues { get; } - /// - /// Gets the count. - /// - int Count { get; } - /// /// Lookup a single item using the specified key. /// /// - /// Fast indexed lookup + /// Fast indexed lookup. /// /// The key. + /// The value that is looked up. Optional Lookup(TKey key); } } \ No newline at end of file diff --git a/src/DynamicData/Cache/IImmutableGroupChangeSet.cs b/src/DynamicData/Cache/IImmutableGroupChangeSet.cs index 60247ac98..70d0fa72e 100644 --- a/src/DynamicData/Cache/IImmutableGroupChangeSet.cs +++ b/src/DynamicData/Cache/IImmutableGroupChangeSet.cs @@ -1,13 +1,18 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// A grouped update collection + /// A grouped update collection. /// - /// The source object type + /// The source object type. /// The type of the key.s - /// The value on which the stream has been grouped + /// The value on which the stream has been grouped. public interface IImmutableGroupChangeSet : IChangeSet, TGroupKey> + where TKey : notnull + where TGroupKey : notnull { } } \ No newline at end of file diff --git a/src/DynamicData/Cache/IIntermediateCache.cs b/src/DynamicData/Cache/IIntermediateCache.cs index a88b150d4..36c798862 100644 --- a/src/DynamicData/Cache/IIntermediateCache.cs +++ b/src/DynamicData/Cache/IIntermediateCache.cs @@ -1,8 +1,9 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; + // ReSharper disable once CheckNamespace namespace DynamicData { @@ -14,13 +15,14 @@ namespace DynamicData /// The type of the object. /// The type of the key. public interface IIntermediateCache : IObservableCache + where TKey : notnull { /// /// Action to apply a batch update to a cache. Multiple update methods can be invoked within a single batch operation. /// These operations are invoked within the cache's lock and is therefore thread safe. - /// The result of the action will produce a single changeset + /// The result of the action will produce a single change set. /// /// The update action. void Edit(Action> updateAction); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IKey.cs b/src/DynamicData/Cache/IKey.cs new file mode 100644 index 000000000..b71c8a6fb --- /dev/null +++ b/src/DynamicData/Cache/IKey.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2011-2020 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 +{ + /// + /// Represents the key of an object. + /// + /// The type of the item. + public interface IKey + { + /// + /// Gets the key. + /// + T Key { get; } + } +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IKeyValue.cs b/src/DynamicData/Cache/IKeyValue.cs index 25160aef9..31077a40b 100644 --- a/src/DynamicData/Cache/IKeyValue.cs +++ b/src/DynamicData/Cache/IKeyValue.cs @@ -1,28 +1,19 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// A keyed value + /// A keyed value. /// /// The type of the object. /// The type of the key. public interface IKeyValue : IKey { /// - /// The value + /// Gets the value. /// TObject Value { get; } } - - /// - /// Represents the key of an object - /// - /// - public interface IKey - { - /// - /// The key. - /// - T Key { get; } - } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IKeyValueCollection.cs b/src/DynamicData/Cache/IKeyValueCollection.cs index b04c158d6..d1b060f5d 100644 --- a/src/DynamicData/Cache/IKeyValueCollection.cs +++ b/src/DynamicData/Cache/IKeyValueCollection.cs @@ -1,8 +1,9 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; + // ReSharper disable once CheckNamespace namespace DynamicData { @@ -14,7 +15,7 @@ namespace DynamicData public interface IKeyValueCollection : IEnumerable> { /// - /// Gets the comparer used to peform the sort + /// Gets the comparer used to perform the sort. /// /// /// The comparer. @@ -22,7 +23,7 @@ public interface IKeyValueCollection : IEnumerable> Comparer { get; } /// - /// The count of items. + /// Gets the count of items. /// /// /// The count. @@ -30,20 +31,20 @@ public interface IKeyValueCollection : IEnumerable - /// Gets the reason for a sort being applied. + /// Gets the optimisations used to produce the sort. /// /// - /// The sort reason. + /// The optimisations. /// - SortReason SortReason { get; } + SortOptimisations Optimisations { get; } /// - /// Gets the optimisations used to produce the sort + /// Gets the reason for a sort being applied. /// /// - /// The optimisations. + /// The sort reason. /// - SortOptimisations Optimisations { get; } + SortReason SortReason { get; } /// /// Gets the element at the specified index in the read-only list. @@ -53,7 +54,7 @@ public interface IKeyValueCollection : IEnumerable /// The zero-based index of the element to get. - /// + /// The key value pair. KeyValuePair this[int index] { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IObservableCache.cs b/src/DynamicData/Cache/IObservableCache.cs index 0eaec8b81..20092848f 100644 --- a/src/DynamicData/Cache/IObservableCache.cs +++ b/src/DynamicData/Cache/IObservableCache.cs @@ -1,64 +1,40 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; + // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// A cache for observing and querying in memory data + /// A cache for observing and querying in memory data. With additional data access operators. /// /// The type of the object. /// The type of the key. - public interface IConnectableCache + public interface IObservableCache : IConnectableCache, IDisposable + where TKey : notnull { /// - /// Returns an observable of any changes which match the specified key. The sequence starts with the initial item in the cache (if there is one). - /// - /// The key. - IObservable> Watch(TKey key); - - /// - /// Returns a filtered stream of cache changes preceded with the initial filtered state + /// Gets the total count of cached items. /// - /// The result will be filtered using the specified predicate. - IObservable> Connect(Func predicate = null); + int Count { get; } /// - /// Returns a filtered stream of cache changes. - /// Unlike Connect(), the returned observable is not prepended with the caches initial items. + /// Gets the Items. /// - /// The result will be filtered using the specified predicate. - IObservable> Preview(Func predicate = null); + IEnumerable Items { get; } /// - /// A count changed observable starting with the current count - /// - IObservable CountChanged { get; } - } - - /// - /// /// A cache for observing and querying in memory data. With additional data access operators - /// - /// The type of the object. - /// The type of the key. - public interface IObservableCache : IConnectableCache, IDisposable - { - /// - /// Gets the keys + /// Gets the keys. /// IEnumerable Keys { get; } /// - /// Gets the Items - /// - IEnumerable Items { get; } - - /// - /// Gets the key value pairs + /// Gets the key value pairs. /// IEnumerable> KeyValues { get; } @@ -66,15 +42,10 @@ public interface IObservableCache : IConnectableCache /// - /// Fast indexed lookup + /// Fast indexed lookup. /// /// The key. - /// + /// An optional with the looked up value. Optional Lookup(TKey key); - - /// - /// The total count of cached items - /// - int Count { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IPageRequest.cs b/src/DynamicData/Cache/IPageRequest.cs index 686a9c1cb..b13912daa 100644 --- a/src/DynamicData/Cache/IPageRequest.cs +++ b/src/DynamicData/Cache/IPageRequest.cs @@ -1,19 +1,22 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// Represents a new page request + /// Represents a new page request. /// public interface IPageRequest { /// - /// The page to move to + /// Gets the page to move to. /// int Page { get; } /// - /// The page size + /// Gets the page size. /// int Size { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IPageResponse.cs b/src/DynamicData/Cache/IPageResponse.cs index b30c8f4e8..87f13fa32 100644 --- a/src/DynamicData/Cache/IPageResponse.cs +++ b/src/DynamicData/Cache/IPageResponse.cs @@ -1,29 +1,32 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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.Operators { /// - /// Response from the pagation operator + /// Response from the pagination operator. /// public interface IPageResponse { /// - /// The size of the page. + /// Gets the current page. /// - int PageSize { get; } + int Page { get; } /// - /// The current page + /// Gets total number of pages. /// - int Page { get; } + int Pages { get; } /// - /// Total number of pages. + /// Gets the size of the page. /// - int Pages { get; } + int PageSize { get; } /// - /// The total number of records in the underlying cache + /// Gets the total number of records in the underlying cache. /// int TotalSize { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IPagedChangeSet.cs b/src/DynamicData/Cache/IPagedChangeSet.cs index b4f62d5b4..b2108d926 100644 --- a/src/DynamicData/Cache/IPagedChangeSet.cs +++ b/src/DynamicData/Cache/IPagedChangeSet.cs @@ -1,21 +1,23 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using DynamicData.Operators; + // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// A paged update collection + /// A paged update collection. /// /// The type of the object. /// The type of the key. public interface IPagedChangeSet : ISortedChangeSet + where TKey : notnull { /// - /// The parameters used to virtualise the stream + /// Gets the parameters used to virtualise the stream. /// IPageResponse Response { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IQuery.cs b/src/DynamicData/Cache/IQuery.cs index 1641f1bef..34198d019 100644 --- a/src/DynamicData/Cache/IQuery.cs +++ b/src/DynamicData/Cache/IQuery.cs @@ -1,40 +1,38 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; + using DynamicData.Kernel; + // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Exposes internal cache state to enable querying + /// Exposes internal cache state to enable querying. /// /// The type of the object. /// The type of the key. public interface IQuery { /// - /// Lookup a single item using the specified key. + /// Gets the count. /// - /// - /// Fast indexed lookup - /// - /// The key. - Optional Lookup(TKey key); + int Count { get; } /// - /// Gets the keys. + /// Gets the items. /// - IEnumerable Keys { get; } + IEnumerable Items { get; } /// - /// Gets the items. + /// Gets the keys. /// - IEnumerable Items { get; } + IEnumerable Keys { get; } /// - /// Gets the items together with their keys + /// Gets the items together with their keys. /// /// /// The key values. @@ -42,8 +40,13 @@ public interface IQuery IEnumerable> KeyValues { get; } /// - /// Gets the count. + /// Lookup a single item using the specified key. /// - int Count { get; } + /// + /// Fast indexed lookup. + /// + /// The key. + /// The looked up value. + Optional Lookup(TKey key); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ISortedChangeSet.cs b/src/DynamicData/Cache/ISortedChangeSet.cs index 5e40775ff..d9a613222 100644 --- a/src/DynamicData/Cache/ISortedChangeSet.cs +++ b/src/DynamicData/Cache/ISortedChangeSet.cs @@ -1,16 +1,20 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// An update collection as per the system convention additionally providing a sorted set of the underling state + /// An update collection as per the system convention additionally providing a sorted set of the underling state. /// /// The type of the object. /// The type of the key. public interface ISortedChangeSet : IChangeSet + where TKey : notnull { /// - /// All cached items in sort order + /// Gets all cached items in sort order. /// IKeyValueCollection SortedItems { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ISortedChangeSetAdaptor.cs b/src/DynamicData/Cache/ISortedChangeSetAdaptor.cs index 6726c83d1..1f9f8e94e 100644 --- a/src/DynamicData/Cache/ISortedChangeSetAdaptor.cs +++ b/src/DynamicData/Cache/ISortedChangeSetAdaptor.cs @@ -1,12 +1,16 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// A simple adaptor to inject side effects into a sorted changeset observable + /// A simple adaptor to inject side effects into a sorted change set observable. /// /// The type of the object. /// The type of the key. public interface ISortedChangeSetAdaptor + where TKey : notnull { /// /// Adapts the specified change. @@ -14,4 +18,4 @@ public interface ISortedChangeSetAdaptor /// The change. void Adapt(ISortedChangeSet change); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ISourceCache.cs b/src/DynamicData/Cache/ISourceCache.cs index d106b6d27..8cc5c73ef 100644 --- a/src/DynamicData/Cache/ISourceCache.cs +++ b/src/DynamicData/Cache/ISourceCache.cs @@ -1,30 +1,32 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; + // ReSharper disable once CheckNamespace namespace DynamicData { /// /// An observable cache which exposes an update API. Used at the root - /// of all observable chains + /// of all observable chains. /// /// The type of the object. /// The type of the key. public interface ISourceCache : IObservableCache + where TKey : notnull { + /// + /// Gets key selector used by the cache to retrieve keys from objects. + /// + Func KeySelector { get; } + /// /// Action to apply a batch update to a cache. Multiple update methods can be invoked within a single batch operation. /// These operations are invoked within the cache's lock and is therefore thread safe. - /// The result of the action will produce a single changeset + /// The result of the action will produce a single change set. /// /// The update action. void Edit(Action> updateAction); - - /// - /// Key selector used by the cache to retrieve keys from objects - /// - Func KeySelector { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ISourceUpdater.cs b/src/DynamicData/Cache/ISourceUpdater.cs index fbc054254..f851d8938 100644 --- a/src/DynamicData/Cache/ISourceUpdater.cs +++ b/src/DynamicData/Cache/ISourceUpdater.cs @@ -1,16 +1,17 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + // ReSharper disable once CheckNamespace namespace DynamicData { /// /// API for updating a source cache. /// - /// Use edit to produce singular changeset. + /// Use edit to produce singular change set. /// /// NB: The evaluate method is used to signal to any observing operators /// to reevaluate whether the object still matches downstream operators. @@ -20,13 +21,8 @@ namespace DynamicData /// The type of the object. /// The type of the key. public interface ISourceUpdater : ICacheUpdater + where TKey : notnull { - /// - /// Clears existing values and loads the specified items. - /// - /// The items. - void Load(IEnumerable items); - /// /// Adds or changes the specified items. /// @@ -50,27 +46,33 @@ public interface ISourceUpdater : ICacheUpdater /// Refreshes the specified items. /// /// The items. - void Refresh(IEnumerable items); + [Obsolete(Constants.EvaluateIsDead)] + void Evaluate(IEnumerable items); /// /// Refreshes the specified item. /// /// The item. - void Refresh(TObject item); + [Obsolete(Constants.EvaluateIsDead)] + void Evaluate(TObject item); + + /// + /// Clears existing values and loads the specified items. + /// + /// The items. + void Load(IEnumerable items); /// /// Refreshes the specified items. /// /// The items. - [Obsolete(Constants.EvaluateIsDead)] - void Evaluate(IEnumerable items); + void Refresh(IEnumerable items); /// /// Refreshes the specified item. /// /// The item. - [Obsolete(Constants.EvaluateIsDead)] - void Evaluate(TObject item); + void Refresh(TObject item); /// /// Removes the specified items. @@ -84,4 +86,4 @@ public interface ISourceUpdater : ICacheUpdater /// The item. void Remove(TObject item); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IVirtualChangeSet.cs b/src/DynamicData/Cache/IVirtualChangeSet.cs index 091edf3e4..b5d940fcf 100644 --- a/src/DynamicData/Cache/IVirtualChangeSet.cs +++ b/src/DynamicData/Cache/IVirtualChangeSet.cs @@ -1,16 +1,20 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// Represents a subset of data reduced by a defined set of parameters + /// Represents a subset of data reduced by a defined set of parameters. /// /// The type of the object. /// The type of the key. public interface IVirtualChangeSet : ISortedChangeSet + where TKey : notnull { /// - /// The parameters used to virtualise the stream + /// Gets the parameters used to virtualise the stream. /// IVirtualResponse Response { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IVirtualParameters.cs b/src/DynamicData/Cache/IVirtualParameters.cs deleted file mode 100644 index 46add2fbe..000000000 --- a/src/DynamicData/Cache/IVirtualParameters.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ReSharper disable once CheckNamespace -namespace DynamicData -{ - /// - /// Defines values used to virtualise the result set - /// - public interface IVirtualResponse - { - /// - /// The requested size of the virtualised data - /// - int Size { get; } - - /// - /// The start index. - /// - int StartIndex { get; } - - /// - /// Gets the total size of the underlying cache - /// - /// - /// The total size. - /// - int TotalSize { get; } - } -} diff --git a/src/DynamicData/Cache/IVirtualRequest.cs b/src/DynamicData/Cache/IVirtualRequest.cs index eb3bf4e6a..bcbc1cdc0 100644 --- a/src/DynamicData/Cache/IVirtualRequest.cs +++ b/src/DynamicData/Cache/IVirtualRequest.cs @@ -1,19 +1,22 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// A request to virtualise a stream + /// A request to virtualise a stream. /// public interface IVirtualRequest { /// - /// The number of records to return + /// Gets the number of records to return. /// int Size { get; } /// - /// The start index + /// Gets the start index. /// int StartIndex { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IVirtualResponse.cs b/src/DynamicData/Cache/IVirtualResponse.cs new file mode 100644 index 000000000..364000bb9 --- /dev/null +++ b/src/DynamicData/Cache/IVirtualResponse.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2011-2020 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 +{ + /// + /// Defines values used to virtualise the result set. + /// + public interface IVirtualResponse + { + /// + /// Gets the requested size of the virtualised data. + /// + int Size { get; } + + /// + /// Gets the start index. + /// + int StartIndex { get; } + + /// + /// Gets the total size of the underlying cache. + /// + /// + /// The total size. + /// + int TotalSize { get; } + } +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IndexedItem.cs b/src/DynamicData/Cache/IndexedItem.cs index 10ed91241..8424ed638 100644 --- a/src/DynamicData/Cache/IndexedItem.cs +++ b/src/DynamicData/Cache/IndexedItem.cs @@ -1,17 +1,19 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. +using System; using System.Collections.Generic; + // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// An item with it's index + /// An item with it's index. /// /// The type of the object. /// The type of the key. - public sealed class IndexedItem //: IIndexedItem + public sealed class IndexedItem : IEquatable> // : IIndexedItem { /// /// Initializes a new instance of the class. @@ -26,52 +28,25 @@ public IndexedItem(TObject value, TKey key, int index) Key = key; } - #region Properties - /// /// Gets the index. /// public int Index { get; } - /// - /// Gets the value. - /// - public TObject Value { get; } - /// /// Gets the key. /// public TKey Key { get; } - #endregion - - #region Equality - - private bool Equals(IndexedItem other) - { - return EqualityComparer.Default.Equals(Key, other.Key) && - EqualityComparer.Default.Equals(Value, other.Value) && Index == other.Index; - } + /// + /// Gets the value. + /// + public TObject Value { get; } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((IndexedItem)obj); + return obj is IndexedItem key && Equals(key); } /// @@ -79,16 +54,25 @@ public override int GetHashCode() { unchecked { - int hashCode = EqualityComparer.Default.GetHashCode(Key); - hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Value); + int hashCode = Key is null ? 0 : EqualityComparer.Default.GetHashCode(Key); + hashCode = (hashCode * 397) ^ (Value is null ? 0 : EqualityComparer.Default.GetHashCode(Value)); hashCode = (hashCode * 397) ^ Index; return hashCode; } } - #endregion - /// public override string ToString() => $"Value: {Value}, Key: {Key}, CurrentIndex: {Index}"; + + /// + public bool Equals(IndexedItem? other) + { + if (other is null) + { + return false; + } + + return EqualityComparer.Default.Equals(Key, other.Key) && EqualityComparer.Default.Equals(Value, other.Value) && Index == other.Index; + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/IntermediateCache.cs b/src/DynamicData/Cache/IntermediateCache.cs index 2f550c4f8..791ce7bea 100644 --- a/src/DynamicData/Cache/IntermediateCache.cs +++ b/src/DynamicData/Cache/IntermediateCache.cs @@ -1,10 +1,12 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; + // ReSharper disable once CheckNamespace namespace DynamicData { @@ -15,22 +17,23 @@ namespace DynamicData /// The type of the object. /// The type of the key. public sealed class IntermediateCache : IIntermediateCache + where TKey : notnull { - private readonly ObservableCache _innnerCache; + private readonly ObservableCache _innerCache; /// /// Initializes a new instance of the class. /// /// The source. - /// source + /// source. public IntermediateCache(IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - _innnerCache = new ObservableCache(source); + _innerCache = new ObservableCache(source); } /// @@ -38,108 +41,104 @@ public IntermediateCache(IObservable> source) /// public IntermediateCache() { - _innnerCache = new ObservableCache(); + _innerCache = new ObservableCache(); } - #region Delegated Members - /// - /// Action to apply a batch update to a cache. Multiple update methods can be invoked within a single batch operation. - /// These operations are invoked within the cache's lock and is therefore thread safe. - /// The result of the action will produce a single changeset + /// Gets the total count of cached items. /// - /// The update action. - public void Edit(Action> updateAction) - { - _innnerCache.UpdateFromIntermediate(updateAction); - } + public int Count => _innerCache.Count; /// - /// A count changed observable starting with the current count + /// Gets a count changed observable starting with the current count. /// - public IObservable CountChanged => _innnerCache.CountChanged; + public IObservable CountChanged => _innerCache.CountChanged; /// - /// Returns a filtered changeset of cache changes preceded with the initial state + /// Gets the Items. /// - /// The predicate. - /// - public IObservable> Connect(Func predicate) - { - return _innnerCache.Connect(predicate); - } + public IEnumerable Items => _innerCache.Items; /// - /// Returns a observable of cache changes preceded with the initial cache state + /// Gets the keys. /// - /// - public IObservable> Connect() - { - return _innnerCache.Connect(); - } - - /// - public IObservable> Preview(Func predicate = null) - { - return _innnerCache.Preview(predicate); - } + public IEnumerable Keys => _innerCache.Keys; /// - /// Returns an observable of any changes which match the specified key. The sequence starts with the initial item in the cache (if there is one). + /// Gets the key value pairs. /// - /// The key. - /// - public IObservable> Watch(TKey key) - { - return _innnerCache.Watch(key); - } + public IEnumerable> KeyValues => _innerCache.KeyValues; /// - /// The total count of cached items + /// Returns a filtered change set of cache changes preceded with the initial state. /// - public int Count => _innnerCache.Count; + /// The predicate. + /// An observable which will emit change sets. + public IObservable> Connect(Func? predicate) + { + return _innerCache.Connect(predicate); + } /// - /// Gets the Items + /// Returns a observable of cache changes preceded with the initial cache state. /// - public IEnumerable Items => _innnerCache.Items; + /// An observable which will emit change sets. + public IObservable> Connect() + { + return _innerCache.Connect(); + } /// - /// Gets the key value pairs + /// Releases unmanaged and - optionally - managed resources. /// - public IEnumerable> KeyValues => _innnerCache.KeyValues; + public void Dispose() + { + _innerCache.Dispose(); + } /// - /// Gets the keys + /// Action to apply a batch update to a cache. Multiple update methods can be invoked within a single batch operation. + /// These operations are invoked within the cache's lock and is therefore thread safe. + /// The result of the action will produce a single change set. /// - public IEnumerable Keys => _innnerCache.Keys; + /// The update action. + public void Edit(Action> updateAction) + { + _innerCache.UpdateFromIntermediate(updateAction); + } /// /// Lookup a single item using the specified key. /// /// - /// Fast indexed lookup + /// Fast indexed lookup. /// /// The key. - /// + /// A optional value. public Optional Lookup(TKey key) { - return _innnerCache.Lookup(key); + return _innerCache.Lookup(key); } - internal IChangeSet GetInitialUpdates(Func filter = null) + /// + public IObservable> Preview(Func? predicate = null) { - return _innnerCache.GetInitialUpdates(filter); + return _innerCache.Preview(predicate); } /// - /// Releases unmanaged and - optionally - managed resources. + /// Returns an observable of any changes which match the specified key. The sequence starts with the initial item in the cache (if there is one). /// - public void Dispose() + /// The key. + /// An observable which emits changes. + public IObservable> Watch(TKey key) { - _innnerCache.Dispose(); + return _innerCache.Watch(key); } - #endregion + internal IChangeSet GetInitialUpdates(Func? filter = null) + { + return _innerCache.GetInitialUpdates(filter); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/AbstractFilter.cs b/src/DynamicData/Cache/Internal/AbstractFilter.cs index 1d3f44ec9..2d3c49e29 100644 --- a/src/DynamicData/Cache/Internal/AbstractFilter.cs +++ b/src/DynamicData/Cache/Internal/AbstractFilter.cs @@ -1,54 +1,49 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal abstract class AbstractFilter : IFilter + where TKey : notnull { private readonly ChangeAwareCache _cache; - protected AbstractFilter(ChangeAwareCache cache, Func filter) + protected AbstractFilter(ChangeAwareCache cache, Func? filter) { _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - if (filter == null) - { - Filter = t => true; - } - else - { - Filter = filter; - } + Filter = filter ?? (_ => true); } public Func Filter { get; } public IChangeSet Refresh(IEnumerable> items) { - //this is an internal method only so we can be sure there are no duplicate keys in the result - //(therefore safe to parallelise) + // this is an internal method only so we can be sure there are no duplicate keys in the result + // (therefore safe to parallelise) Optional> Factory(KeyValuePair kv) { - var exisiting = _cache.Lookup(kv.Key); + var existing = _cache.Lookup(kv.Key); var matches = Filter(kv.Value); if (matches) { - if (!exisiting.HasValue) + if (!existing.HasValue) { return new Change(ChangeReason.Add, kv.Key, kv.Value); } } else { - if (exisiting.HasValue) + if (existing.HasValue) { - return new Change(ChangeReason.Remove, kv.Key, kv.Value, exisiting); + return new Change(ChangeReason.Remove, kv.Key, kv.Value, existing); } } @@ -61,21 +56,20 @@ Optional> Factory(KeyValuePair kv) return _cache.CaptureChanges(); } - protected abstract IEnumerable> Refresh(IEnumerable> items, Func, Optional>> factory); - public IChangeSet Update(IChangeSet updates) { - var withfilter = GetChangesWithFilter(updates); - return ProcessResult(withfilter); + var withFilter = GetChangesWithFilter(updates); + return ProcessResult(withFilter); } protected abstract IEnumerable GetChangesWithFilter(IChangeSet updates); + protected abstract IEnumerable> Refresh(IEnumerable> items, Func, Optional>> factory); + private IChangeSet ProcessResult(IEnumerable source) { - //Have to process one item at a time as an item can be included multiple - //times in any batch - + // Have to process one item at a time as an item can be included multiple + // times in any batch foreach (var item in source) { var matches = item.IsMatch; @@ -93,6 +87,7 @@ private IChangeSet ProcessResult(IEnumerable so } break; + case ChangeReason.Update: { if (matches) @@ -106,15 +101,17 @@ private IChangeSet ProcessResult(IEnumerable so } break; + case ChangeReason.Remove: _cache.Remove(u.Key); break; + case ChangeReason.Refresh: { - var exisiting = _cache.Lookup(key); + var existing = _cache.Lookup(key); if (matches) { - if (!exisiting.HasValue) + if (!existing.HasValue) { _cache.AddOrUpdate(u.Current, u.Key); } @@ -125,7 +122,7 @@ private IChangeSet ProcessResult(IEnumerable so } else { - if (exisiting.HasValue) + if (existing.HasValue) { _cache.Remove(u.Key); } @@ -139,11 +136,14 @@ private IChangeSet ProcessResult(IEnumerable so return _cache.CaptureChanges(); } - protected struct UpdateWithFilter + protected readonly struct UpdateWithFilter { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// + /// If the filter is a match. + /// The change. public UpdateWithFilter(bool isMatch, Change change) { IsMatch = isMatch; @@ -151,7 +151,8 @@ public UpdateWithFilter(bool isMatch, Change change) } public Change Change { get; } + public bool IsMatch { get; } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/AnonymousObservableCache.cs b/src/DynamicData/Cache/Internal/AnonymousObservableCache.cs index 27da495fa..0e24f119c 100644 --- a/src/DynamicData/Cache/Internal/AnonymousObservableCache.cs +++ b/src/DynamicData/Cache/Internal/AnonymousObservableCache.cs @@ -1,20 +1,22 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class AnonymousObservableCache : IObservableCache + where TKey : notnull { private readonly IObservableCache _cache; public AnonymousObservableCache(IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -27,39 +29,39 @@ public AnonymousObservableCache(IObservableCache cache) _cache = cache ?? throw new ArgumentNullException(nameof(cache)); } + public int Count => _cache.Count; + public IObservable CountChanged => _cache.CountChanged; - public IObservable> Watch(TKey key) - { - return _cache.Watch(key); - } + public IEnumerable Items => _cache.Items; - public IObservable> Connect(Func predicate = null) + public IEnumerable Keys => _cache.Keys; + + public IEnumerable> KeyValues => _cache.KeyValues; + + public IObservable> Connect(Func? predicate = null) { return _cache.Connect(predicate); } - public IObservable> Preview(Func predicate = null) + public void Dispose() { - return _cache.Preview(predicate); + _cache.Dispose(); } - public IEnumerable Keys => _cache.Keys; - - public IEnumerable Items => _cache.Items; - - public int Count => _cache.Count; - - public IEnumerable> KeyValues => _cache.KeyValues; - public Optional Lookup(TKey key) { return _cache.Lookup(key); } - public void Dispose() + public IObservable> Preview(Func? predicate = null) { - _cache.Dispose(); + return _cache.Preview(predicate); + } + + public IObservable> Watch(TKey key) + { + return _cache.Watch(key); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/AnonymousQuery.cs b/src/DynamicData/Cache/Internal/AnonymousQuery.cs index 1a61db194..75b3d4586 100644 --- a/src/DynamicData/Cache/Internal/AnonymousQuery.cs +++ b/src/DynamicData/Cache/Internal/AnonymousQuery.cs @@ -1,13 +1,15 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class AnonymousQuery : IQuery + where TKey : notnull { private readonly Cache _cache; @@ -20,13 +22,13 @@ public AnonymousQuery(Cache cache) public IEnumerable Items => _cache.Items; - public IEnumerable> KeyValues => _cache.KeyValues; - public IEnumerable Keys => _cache.Keys; + public IEnumerable> KeyValues => _cache.KeyValues; + public Optional Lookup(TKey key) { return _cache.Lookup(key); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/AutoRefresh.cs b/src/DynamicData/Cache/Internal/AutoRefresh.cs index 7a8b5f3cc..84dac90af 100644 --- a/src/DynamicData/Cache/Internal/AutoRefresh.cs +++ b/src/DynamicData/Cache/Internal/AutoRefresh.cs @@ -1,9 +1,8 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; -using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -11,58 +10,45 @@ namespace DynamicData.Cache.Internal { internal class AutoRefresh + where TKey : notnull { - private readonly IObservable> _source; - private readonly Func> _reevaluator; private readonly TimeSpan? _buffer; + + private readonly Func> _reEvaluator; + private readonly IScheduler _scheduler; - public AutoRefresh(IObservable> source, - Func> reevaluator, - TimeSpan? buffer = null, - IScheduler scheduler = null) + private readonly IObservable> _source; + + public AutoRefresh(IObservable> source, Func> reEvaluator, TimeSpan? buffer = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); - _reevaluator = reevaluator ?? throw new ArgumentNullException(nameof(reevaluator)); + _reEvaluator = reEvaluator ?? throw new ArgumentNullException(nameof(reEvaluator)); _buffer = buffer; - _scheduler = scheduler; + _scheduler = scheduler ?? Scheduler.Default; } public IObservable> Run() { - return Observable.Create>(observer => - { - var shared = _source.Publish(); + return Observable.Create>( + observer => + { + var shared = _source.Publish(); - //monitor each item observable and create change - var changes = shared.MergeMany((t, k) => - { - return _reevaluator(t, k) - .Select(_ => new Change(ChangeReason.Refresh, k, t)); - }); + // monitor each item observable and create change + var changes = shared.MergeMany((t, k) => { return _reEvaluator(t, k).Select(_ => new Change(ChangeReason.Refresh, k, t)); }); - //create a changeset, either buffered or one item at the time - IObservable> refreshChanges; - if (_buffer == null) - { - refreshChanges = changes.Select(c => new ChangeSet(new[] { c })); - } - else - { - refreshChanges = changes.Buffer(_buffer.Value, _scheduler ?? Scheduler.Default) - .Where(list => list.Any()) - .Select(items => new ChangeSet(items)); - } + // create a change set, either buffered or one item at the time + IObservable> refreshChanges = _buffer is null ? + changes.Select(c => new ChangeSet(new[] { c })) : + changes.Buffer(_buffer.Value, _scheduler).Where(list => list.Count > 0).Select(items => new ChangeSet(items)); - //publish refreshes and underlying changes - var locker = new object(); - var publisher = shared.Synchronize(locker) - .Merge(refreshChanges.Synchronize(locker)) - .SubscribeSafe(observer); + // publish refreshes and underlying changes + var locker = new object(); + var publisher = shared.Synchronize(locker).Merge(refreshChanges.Synchronize(locker)).SubscribeSafe(observer); - return new CompositeDisposable(publisher, shared.Connect()); - }); + return new CompositeDisposable(publisher, shared.Connect()); + }); } } - -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/BatchIf.cs b/src/DynamicData/Cache/Internal/BatchIf.cs index 7c7a5a96b..5c0267f64 100644 --- a/src/DynamicData/Cache/Internal/BatchIf.cs +++ b/src/DynamicData/Cache/Internal/BatchIf.cs @@ -1,32 +1,33 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; +using System.Collections.Generic; +using System.Linq; using System.Reactive; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Collections.Generic; -using System.Linq; namespace DynamicData.Cache.Internal { internal sealed class BatchIf + where TKey : notnull { - private readonly IObservable> _source; - private readonly IObservable _pauseIfTrueSelector; - private readonly TimeSpan? _timeOut; private readonly bool _initialPauseState; - private readonly IObservable _intervalTimer; + + private readonly IObservable? _intervalTimer; + + private readonly IObservable _pauseIfTrueSelector; + private readonly IScheduler _scheduler; - public BatchIf(IObservable> source, - IObservable pauseIfTrueSelector, - TimeSpan? timeOut, - bool initialPauseState = false, - IObservable intervalTimer =null, - IScheduler scheduler = null) + private readonly IObservable> _source; + + private readonly TimeSpan? _timeOut; + + public BatchIf(IObservable> source, IObservable pauseIfTrueSelector, TimeSpan? timeOut, bool initialPauseState = false, IObservable? intervalTimer = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); _pauseIfTrueSelector = pauseIfTrueSelector ?? throw new ArgumentNullException(nameof(pauseIfTrueSelector)); @@ -38,101 +39,93 @@ public BatchIf(IObservable> source, public IObservable> Run() { - return Observable.Create> - ( + return Observable.Create>( observer => - { - var batchedChanges = new List>(); - var locker = new object(); - var paused = _initialPauseState; - var timeoutDisposer = new SerialDisposable(); - var intervalTimerDisposer = new SerialDisposable(); - - void ResumeAction() { - if (batchedChanges.Count == 0) - { - return; - } + var batchedChanges = new List>(); + var locker = new object(); + var paused = _initialPauseState; + var timeoutDisposer = new SerialDisposable(); + var intervalTimerDisposer = new SerialDisposable(); - var resultingBatch = new ChangeSet(batchedChanges.Select(cs=>cs.Count).Sum()); - foreach (var cs in batchedChanges) + void ResumeAction() { - resultingBatch.AddRange(cs); - } - - observer.OnNext(resultingBatch); - batchedChanges.Clear(); - } + if (batchedChanges.Count == 0) + { + return; + } - IDisposable IntervalFunction() - { - return _intervalTimer - .Synchronize(locker) - .Finally(() => paused = false) - .Subscribe(_ => + var resultingBatch = new ChangeSet(batchedChanges.Select(cs => cs.Count).Sum()); + foreach (var cs in batchedChanges) { - paused = false; - ResumeAction(); - if (_intervalTimer!=null) - { - paused = true; - } - }); - } + resultingBatch.AddRange(cs); + } - if (_intervalTimer != null) - { - intervalTimerDisposer.Disposable = IntervalFunction(); - } + observer.OnNext(resultingBatch); + batchedChanges.Clear(); + } - var pausedHandler = _pauseIfTrueSelector - .Synchronize(locker) - .Subscribe(p => + IDisposable IntervalFunction() { - paused = p; - if (!p) - { - //pause window has closed, so reset timer - if (_timeOut.HasValue) - { - timeoutDisposer.Disposable = Disposable.Empty; - } + return _intervalTimer.Synchronize(locker).Finally(() => paused = false).Subscribe( + _ => + { + paused = false; + ResumeAction(); + if (_intervalTimer is not null) + { + paused = true; + } + }); + } - ResumeAction(); - } - else - { - if (_timeOut.HasValue) + if (_intervalTimer is not null) + { + intervalTimerDisposer.Disposable = IntervalFunction(); + } + + var pausedHandler = _pauseIfTrueSelector.Synchronize(locker).Subscribe( + p => { - timeoutDisposer.Disposable = Observable.Timer(_timeOut.Value, _scheduler) - .Synchronize(locker) - .Subscribe(_ => + paused = p; + if (!p) + { + // pause window has closed, so reset timer + if (_timeOut.HasValue) { - paused = false; - ResumeAction(); - }); - } - } + timeoutDisposer.Disposable = Disposable.Empty; + } - }); + ResumeAction(); + } + else + { + if (_timeOut.HasValue) + { + timeoutDisposer.Disposable = Observable.Timer(_timeOut.Value, _scheduler).Synchronize(locker).Subscribe( + _ => + { + paused = false; + ResumeAction(); + }); + } + } + }); - var publisher = _source - .Synchronize(locker) - .Subscribe(changes => - { - batchedChanges.Add(changes); + var publisher = _source.Synchronize(locker).Subscribe( + changes => + { + batchedChanges.Add(changes); - //publish if not paused - if (!paused) - { - ResumeAction(); - } - }); + // publish if not paused + if (!paused) + { + ResumeAction(); + } + }); - return new CompositeDisposable(publisher, pausedHandler, timeoutDisposer, intervalTimerDisposer); - } - ); + return new CompositeDisposable(publisher, pausedHandler, timeoutDisposer, intervalTimerDisposer); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/Cache.cs b/src/DynamicData/Cache/Internal/Cache.cs index 16da28bb8..0c7cc5b5b 100644 --- a/src/DynamicData/Cache/Internal/Cache.cs +++ b/src/DynamicData/Cache/Internal/Cache.cs @@ -1,46 +1,59 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Diagnostics; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { [DebuggerDisplay("Cache<{typeof(TObject).Name}, {typeof(TKey).Name}> ({Count Items)")] internal class Cache : ICache + where TKey : notnull { + public static readonly Cache Empty = new(); + private readonly Dictionary _data; + public Cache(int capacity = -1) + { + _data = capacity > 1 ? new Dictionary(capacity) : new Dictionary(); + } + + public Cache(Dictionary data) + { + _data = data; + } + public int Count => _data.Count; - public IEnumerable> KeyValues => _data; + public IEnumerable Items => _data.Values; + public IEnumerable Keys => _data.Keys; - public static readonly Cache Empty = new Cache(); + public IEnumerable> KeyValues => _data; - public Cache(int capacity = -1) + public void AddOrUpdate(TObject item, TKey key) { - _data = capacity > 1 ? new Dictionary(capacity) : new Dictionary(); + _data[key] = item; } - public Cache(Dictionary data) + public void Clear() { - _data = data; + _data.Clear(); } public Cache Clone() { - return _data== null - ? new Cache() - : new Cache(new Dictionary(_data)); + return new(new Dictionary(_data)); } public void Clone(IChangeSet changes) { - if (changes == null) + if (changes is null) { throw new ArgumentNullException(nameof(changes)); } @@ -56,6 +69,7 @@ public void Clone(IChangeSet changes) } break; + case ChangeReason.Remove: _data.Remove(item.Key); break; @@ -68,17 +82,34 @@ public Optional Lookup(TKey key) return _data.Lookup(key); } - public void AddOrUpdate(TObject item, TKey key) + /// + /// Sends a signal for operators to recalculate it's state. + /// + public void Refresh() + { + } + + /// + /// Refreshes the items matching the specified keys. + /// + /// The keys. + public void Refresh(IEnumerable keys) + { + } + + /// + /// Refreshes the item matching the specified key. + /// + /// The key to refresh. + public void Refresh(TKey key) { - _data[key] = item; } public void Remove(IEnumerable keys) { if (keys is IList list) { - var enumerable = EnumerableIList.Create(list); - foreach (var item in enumerable) + foreach (var item in EnumerableIList.Create(list)) { Remove(item); } @@ -99,35 +130,5 @@ public void Remove(TKey key) _data.Remove(key); } } - - public void Clear() - { - _data.Clear(); - } - - /// - /// Sends a signal for operators to recalculate it's state - /// - public void Refresh() - { - - } - - /// - /// Refreshes the items matching the specified keys - /// - /// The keys. - public void Refresh(IEnumerable keys) - { - - } - - /// - /// Refreshes the item matching the specified key - /// - public void Refresh(TKey key) - { - - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/CacheEx.cs b/src/DynamicData/Cache/Internal/CacheEx.cs index 00235ef02..365c13f2d 100644 --- a/src/DynamicData/Cache/Internal/CacheEx.cs +++ b/src/DynamicData/Cache/Internal/CacheEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,17 +10,10 @@ namespace DynamicData.Cache.Internal { internal static class CacheEx { - - public static IChangeSet GetInitialUpdates(this ChangeAwareCache source, Func filter = null) - { - var filtered = filter == null ? source.KeyValues : source.KeyValues.Where(kv => filter(kv.Value)); - return new ChangeSet(filtered.Select(i => new Change(ChangeReason.Add, i.Key, i.Value))); - } - public static void Clone(this IDictionary source, IChangeSet changes) + where TKey : notnull { - var enumerable = changes.ToConcreteType(); - foreach (var item in enumerable) + foreach (var item in changes.ToConcreteType()) { switch (item.Reason) { @@ -28,11 +21,19 @@ public static void Clone(this IDictionary source, case ChangeReason.Add: source[item.Key] = item.Current; break; + case ChangeReason.Remove: source.Remove(item.Key); break; } } } + + public static IChangeSet GetInitialUpdates(this ChangeAwareCache source, Func? filter = null) + where TKey : notnull + { + var filtered = filter is null ? source.KeyValues : source.KeyValues.Where(kv => filter(kv.Value)); + return new ChangeSet(filtered.Select(i => new Change(ChangeReason.Add, i.Key, i.Value))); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/CacheUpdater.cs b/src/DynamicData/Cache/Internal/CacheUpdater.cs index 16fe23dae..eb6b6db9c 100644 --- a/src/DynamicData/Cache/Internal/CacheUpdater.cs +++ b/src/DynamicData/Cache/Internal/CacheUpdater.cs @@ -1,28 +1,31 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class CacheUpdater : ISourceUpdater + where TKey : notnull { private readonly ICache _cache; - private readonly Func _keySelector; - public CacheUpdater(ICache cache, Func keySelector = null) + private readonly Func? _keySelector; + + public CacheUpdater(ICache cache, Func? keySelector = null) { _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _keySelector = keySelector; } - public CacheUpdater(Dictionary data, Func keySelector = null) + public CacheUpdater(Dictionary data, Func? keySelector = null) { - if (data == null) + if (data is null) { throw new ArgumentNullException(nameof(data)); } @@ -31,46 +34,30 @@ public CacheUpdater(Dictionary data, Func keySelec _keySelector = keySelector; } + public int Count => _cache.Count; + public IEnumerable Items => _cache.Items; public IEnumerable Keys => _cache.Keys; public IEnumerable> KeyValues => _cache.KeyValues; - public Optional Lookup(TKey key) - { - var item = _cache.Lookup(key); - return item.HasValue ? item.Value : Optional.None(); - } - - public void Load(IEnumerable items) - { - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - - Clear(); - AddOrUpdate(items); - } - public void AddOrUpdate(IEnumerable items) { - if (items == null) + if (items is null) { throw new ArgumentNullException(nameof(items)); } - if (_keySelector == null) + if (_keySelector is null) { throw new KeySelectorException("A key selector must be specified"); } if (items is IList list) { - //zero allocation enumerator - var enumerable = EnumerableIList.Create(list); - foreach (var item in enumerable) + // zero allocation enumerator + foreach (var item in EnumerableIList.Create(list)) { _cache.AddOrUpdate(item, _keySelector(item)); } @@ -86,7 +73,7 @@ public void AddOrUpdate(IEnumerable items) public void AddOrUpdate(TObject item) { - if (_keySelector == null) + if (_keySelector is null) { throw new KeySelectorException("A key selector must be specified"); } @@ -97,7 +84,7 @@ public void AddOrUpdate(TObject item) public void AddOrUpdate(TObject item, IEqualityComparer comparer) { - if (_keySelector == null) + if (_keySelector is null) { throw new KeySelectorException("A key selector must be specified"); } @@ -118,9 +105,69 @@ public void AddOrUpdate(TObject item, IEqualityComparer comparer) _cache.AddOrUpdate(item, key); } + public void AddOrUpdate(IEnumerable> itemsPairs) + { + if (itemsPairs is IList> list) + { + // zero allocation enumerator + foreach (var item in EnumerableIList.Create(list)) + { + _cache.AddOrUpdate(item.Value, item.Key); + } + } + else + { + foreach (var item in itemsPairs) + { + _cache.AddOrUpdate(item.Value, item.Key); + } + } + } + + public void AddOrUpdate(KeyValuePair item) + { + _cache.AddOrUpdate(item.Value, item.Key); + } + + public void AddOrUpdate(TObject item, TKey key) + { + _cache.AddOrUpdate(item, key); + } + + public void Clear() + { + _cache.Clear(); + } + + public void Clone(IChangeSet changes) + { + _cache.Clone(changes); + } + + [Obsolete(Constants.EvaluateIsDead)] + public void Evaluate(IEnumerable keys) => Refresh(keys); + + [Obsolete(Constants.EvaluateIsDead)] + public void Evaluate(IEnumerable items) => Refresh(items); + + [Obsolete(Constants.EvaluateIsDead)] + public void Evaluate(TObject item) => Refresh(item); + + [Obsolete(Constants.EvaluateIsDead)] + public void Evaluate() + { + Refresh(); + } + + [Obsolete(Constants.EvaluateIsDead)] + public void Evaluate(TKey key) + { + Refresh(key); + } + public TKey GetKey(TObject item) { - if (_keySelector == null) + if (_keySelector is null) { throw new KeySelectorException("A key selector must be specified"); } @@ -130,7 +177,7 @@ public TKey GetKey(TObject item) public IEnumerable> GetKeyValues(IEnumerable items) { - if (_keySelector == null) + if (_keySelector is null) { throw new KeySelectorException("A key selector must be specified"); } @@ -138,34 +185,32 @@ public IEnumerable> GetKeyValues(IEnumerable new KeyValuePair(_keySelector(t), t)); } - public void AddOrUpdate(IEnumerable> itemsPairs) + public void Load(IEnumerable items) { - if (itemsPairs is IList> list) - { - //zero allocation enumerator - var enumerable = EnumerableIList.Create(list); - foreach (var item in enumerable) - { - _cache.AddOrUpdate(item.Value, item.Key); - } - } - else + if (items is null) { - foreach (var item in itemsPairs) - { - _cache.AddOrUpdate(item.Value, item.Key); - } + throw new ArgumentNullException(nameof(items)); } + + Clear(); + AddOrUpdate(items); } - public void AddOrUpdate(KeyValuePair item) + public Optional Lookup(TKey key) { - _cache.AddOrUpdate(item.Value, item.Key); + var item = _cache.Lookup(key); + return item.HasValue ? item.Value : Optional.None(); } - public void AddOrUpdate(TObject item, TKey key) + public Optional Lookup(TObject item) { - _cache.AddOrUpdate(item, key); + if (_keySelector is null) + { + throw new KeySelectorException("A key selector must be specified"); + } + + TKey key = _keySelector(item); + return Lookup(key); } public void Refresh() @@ -175,16 +220,15 @@ public void Refresh() public void Refresh(IEnumerable items) { - if (items == null) + if (items is null) { throw new ArgumentNullException(nameof(items)); } if (items is IList list) { - //zero allocation enumerator - var enumerable = EnumerableIList.Create(list); - foreach (var item in enumerable) + // zero allocation enumerator + foreach (var item in EnumerableIList.Create(list)) { Refresh(item); } @@ -200,16 +244,15 @@ public void Refresh(IEnumerable items) public void Refresh(IEnumerable keys) { - if (keys == null) + if (keys is null) { throw new ArgumentNullException(nameof(keys)); } if (keys is IList list) { - //zero allocation enumerator - var enumerable = EnumerableIList.Create(list); - foreach (var item in enumerable) + // zero allocation enumerator + foreach (var item in EnumerableIList.Create(list)) { Refresh(item); } @@ -225,7 +268,7 @@ public void Refresh(IEnumerable keys) public void Refresh(TObject item) { - if (_keySelector == null) + if (_keySelector is null) { throw new KeySelectorException("A key selector must be specified"); } @@ -234,44 +277,22 @@ public void Refresh(TObject item) _cache.Refresh(key); } - [Obsolete(Constants.EvaluateIsDead)] - public void Evaluate(IEnumerable keys) => Refresh(keys); - - [Obsolete(Constants.EvaluateIsDead)] - public void Evaluate(IEnumerable items) => Refresh(items); - - [Obsolete(Constants.EvaluateIsDead)] - public void Evaluate(TObject item) => Refresh(item); - public void Refresh(TKey key) { _cache.Refresh(key); } - [Obsolete(Constants.EvaluateIsDead)] - public void Evaluate() - { - Refresh(); - } - - [Obsolete(Constants.EvaluateIsDead)] - public void Evaluate(TKey key) - { - Refresh(key); - } - public void Remove(IEnumerable items) { - if (items == null) + if (items is null) { throw new ArgumentNullException(nameof(items)); } if (items is IList list) { - //zero allocation enumerator - var enumerable = EnumerableIList.Create(list); - foreach (var item in enumerable) + // zero allocation enumerator + foreach (var item in EnumerableIList.Create(list)) { Remove(item); } @@ -287,16 +308,15 @@ public void Remove(IEnumerable items) public void Remove(IEnumerable keys) { - if (keys == null) + if (keys is null) { throw new ArgumentNullException(nameof(keys)); } if (keys is IList list) { - //zero allocation enumerator - var enumerable = EnumerableIList.Create(list); - foreach (var key in enumerable) + // zero allocation enumerator + foreach (var key in EnumerableIList.Create(list)) { Remove(key); } @@ -310,19 +330,9 @@ public void Remove(IEnumerable keys) } } - public void RemoveKeys(IEnumerable keys) - { - if (keys == null) - { - throw new ArgumentNullException(nameof(keys)); - } - - _cache.Remove(keys); - } - public void Remove(TObject item) { - if (_keySelector == null) + if (_keySelector is null) { throw new KeySelectorException("A key selector must be specified"); } @@ -331,39 +341,22 @@ public void Remove(TObject item) _cache.Remove(key); } - public Optional Lookup(TObject item) - { - if (_keySelector == null) - { - throw new KeySelectorException("A key selector must be specified"); - } - - TKey key = _keySelector(item); - return Lookup(key); - } - public void Remove(TKey key) { _cache.Remove(key); } - public void RemoveKey(TKey key) - { - Remove(key); - } - public void Remove(IEnumerable> items) { - if (items == null) + if (items is null) { throw new ArgumentNullException(nameof(items)); } if (items is IList list) { - //zero allocation enumerator - var enumerable = EnumerableIList.Create(list); - foreach (var key in enumerable) + // zero allocation enumerator + foreach (var key in EnumerableIList.Create(list)) { Remove(key); } @@ -382,21 +375,24 @@ public void Remove(KeyValuePair item) Remove(item.Key); } - public void Clear() + public void RemoveKey(TKey key) { - _cache.Clear(); + Remove(key); } - public int Count => _cache.Count; - - public void Update(IChangeSet changes) + public void RemoveKeys(IEnumerable keys) { - _cache.Clone(changes); + if (keys is null) + { + throw new ArgumentNullException(nameof(keys)); + } + + _cache.Remove(keys); } - public void Clone(IChangeSet changes) + public void Update(IChangeSet changes) { _cache.Clone(changes); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/Cast.cs b/src/DynamicData/Cache/Internal/Cast.cs index aa491527f..19a73103c 100644 --- a/src/DynamicData/Cache/Internal/Cast.cs +++ b/src/DynamicData/Cache/Internal/Cast.cs @@ -1,19 +1,22 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Linq; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class Cast + where TKey : notnull { - private readonly IObservable> _source; private readonly Func _converter; + private readonly IObservable> _source; + public Cast(IObservable> source, Func converter) { _source = source ?? throw new ArgumentNullException(nameof(source)); @@ -22,17 +25,12 @@ public Cast(IObservable> source, Func> Run() { - return _source.Select(changes => - { - var transformed = changes.Select(change => new Change(change.Reason, - change.Key, - _converter(change.Current), - change.Previous.Convert(_converter), - change.CurrentIndex, - change.PreviousIndex)); - return new ChangeSet(transformed); - }); + return _source.Select( + changes => + { + var transformed = changes.Select(change => new Change(change.Reason, change.Key, _converter(change.Current), change.Previous.Convert(_converter), change.CurrentIndex, change.PreviousIndex)); + return new ChangeSet(transformed); + }); } } - -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/ChangesReducer.cs b/src/DynamicData/Cache/Internal/ChangesReducer.cs index 570145f1e..c1bbed92a 100644 --- a/src/DynamicData/Cache/Internal/ChangesReducer.cs +++ b/src/DynamicData/Cache/Internal/ChangesReducer.cs @@ -1,8 +1,9 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Diagnostics.Contracts; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal @@ -11,6 +12,7 @@ internal static class ChangesReducer { [Pure] public static Optional> Reduce(Optional> previous, Change next) + where TKey : notnull { if (!previous.HasValue) { @@ -38,4 +40,4 @@ public static Optional> Reduce(Optional - /// How the multiple streams are combinedL + /// How the multiple streams are combinedL. /// public enum CombineOperator { /// - /// Apply a logical And between two or more observable change sets + /// Apply a logical And between two or more observable change sets. /// And, /// - /// Apply a logical Or between two or more observable change sets + /// Apply a logical Or between two or more observable change sets. /// Or, /// - /// Apply a logical Xor between two or more observable change sets + /// Apply a logical Xor between two or more observable change sets. /// Xor, /// - /// Include the items in the first changeset and exclude any items belonging to the other + /// Include the items in the first change set and exclude any items belonging to the other. /// Except } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/Combiner.cs b/src/DynamicData/Cache/Internal/Combiner.cs index ef12ebdd4..56517581b 100644 --- a/src/DynamicData/Cache/Internal/Combiner.cs +++ b/src/DynamicData/Cache/Internal/Combiner.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,20 +6,25 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { /// - /// Combines multiple caches using logical operators + /// Combines multiple caches using logical operators. /// internal sealed class Combiner + where TKey : notnull { + private readonly ChangeAwareCache _combinedCache = new(); + + private readonly object _locker = new(); + private readonly IList> _sourceCaches = new List>(); - private readonly ChangeAwareCache _combinedCache = new ChangeAwareCache(); - private readonly object _locker = new object(); private readonly CombineOperator _type; + private readonly Action> _updatedCallback; public Combiner(CombineOperator type, Action> updatedCallback) @@ -30,7 +35,7 @@ public Combiner(CombineOperator type, Action> updatedC public IDisposable Subscribe(IObservable>[] source) { - //subscribe + // subscribe var disposable = new CompositeDisposable(); lock (_locker) { @@ -47,16 +52,47 @@ public IDisposable Subscribe(IObservable>[] source) return disposable; } + private bool MatchesConstraint(TKey key) + { + switch (_type) + { + case CombineOperator.And: + { + return _sourceCaches.All(s => s.Lookup(key).HasValue); + } + + case CombineOperator.Or: + { + return _sourceCaches.Any(s => s.Lookup(key).HasValue); + } + + case CombineOperator.Xor: + { + return _sourceCaches.Count(s => s.Lookup(key).HasValue) == 1; + } + + case CombineOperator.Except: + { + bool first = _sourceCaches.Take(1).Any(s => s.Lookup(key).HasValue); + bool others = _sourceCaches.Skip(1).Any(s => s.Lookup(key).HasValue); + return first && !others; + } + + default: + throw new ArgumentOutOfRangeException(nameof(key)); + } + } + private void Update(Cache cache, IChangeSet updates) { IChangeSet notifications; lock (_locker) { - //update cache for the individual source + // update cache for the individual source cache.Clone(updates); - //update combined + // update combined notifications = UpdateCombined(updates); } @@ -68,8 +104,7 @@ private void Update(Cache cache, IChangeSet update private IChangeSet UpdateCombined(IChangeSet updates) { - //child caches have been updated before we reached this point. - + // child caches have been updated before we reached this point. foreach (var update in updates) { TKey key = update.Key; @@ -79,7 +114,7 @@ private IChangeSet UpdateCombined(IChangeSet updat case ChangeReason.Update: { // get the current key. - //check whether the item should belong to the cache + // check whether the item should belong to the cache var cached = _combinedCache.Lookup(key); var contained = cached.HasValue; var match = MatchesConstraint(key); @@ -117,9 +152,7 @@ private IChangeSet UpdateCombined(IChangeSet updat if (shouldBeIncluded) { - var firstOne = _sourceCaches.Select(s => s.Lookup(key)) - .SelectValues() - .First(); + var firstOne = _sourceCaches.Select(s => s.Lookup(key)).SelectValues().First(); if (!cached.HasValue) { @@ -152,36 +185,5 @@ private IChangeSet UpdateCombined(IChangeSet updat return _combinedCache.CaptureChanges(); } - - private bool MatchesConstraint(TKey key) - { - switch (_type) - { - case CombineOperator.And: - { - return _sourceCaches.All(s => s.Lookup(key).HasValue); - } - - case CombineOperator.Or: - { - return _sourceCaches.Any(s => s.Lookup(key).HasValue); - } - - case CombineOperator.Xor: - { - return _sourceCaches.Count(s => s.Lookup(key).HasValue) == 1; - } - - case CombineOperator.Except: - { - bool first = _sourceCaches.Take(1).Any(s => s.Lookup(key).HasValue); - bool others = _sourceCaches.Skip(1).Any(s => s.Lookup(key).HasValue); - return first && !others; - } - - default: - throw new ArgumentOutOfRangeException(nameof(key)); - } - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/DeferUntilLoaded.cs b/src/DynamicData/Cache/Internal/DeferUntilLoaded.cs index 230d8c658..7280fc451 100644 --- a/src/DynamicData/Cache/Internal/DeferUntilLoaded.cs +++ b/src/DynamicData/Cache/Internal/DeferUntilLoaded.cs @@ -1,40 +1,32 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class DeferUntilLoaded + where TKey : notnull { private readonly IObservable> _result; - public DeferUntilLoaded([NotNull] IObservableCache source) + public DeferUntilLoaded(IObservableCache source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - _result = source.CountChanged.Where(count => count != 0) - .Take(1) - .Select(_ => new ChangeSet()) - .Concat(source.Connect()) - .NotEmpty(); + _result = source.CountChanged.Where(count => count != 0).Take(1).Select(_ => new ChangeSet()).Concat(source.Connect()).NotEmpty(); } public DeferUntilLoaded(IObservable> source) { - _result = source.MonitorStatus() - .Where(status => status == ConnectionStatus.Loaded) - .Take(1) - .Select(_ => new ChangeSet()) - .Concat(source) - .NotEmpty(); + _result = source.MonitorStatus().Where(status => status == ConnectionStatus.Loaded).Take(1).Select(_ => new ChangeSet()).Concat(source).NotEmpty(); } public IObservable> Run() diff --git a/src/DynamicData/Cache/Internal/DictionaryExtensions.cs b/src/DynamicData/Cache/Internal/DictionaryExtensions.cs index 54e0cb665..acab4550f 100644 --- a/src/DynamicData/Cache/Internal/DictionaryExtensions.cs +++ b/src/DynamicData/Cache/Internal/DictionaryExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -19,4 +19,4 @@ internal static IEnumerable GetOrEmpty(this IDictionary(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/DisposeMany.cs b/src/DynamicData/Cache/Internal/DisposeMany.cs index ba26f968e..5ccee7e8b 100644 --- a/src/DynamicData/Cache/Internal/DisposeMany.cs +++ b/src/DynamicData/Cache/Internal/DisposeMany.cs @@ -1,21 +1,23 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class DisposeMany + where TKey : notnull { - private readonly IObservable> _source; private readonly Action _removeAction; - public DisposeMany([NotNull] IObservable> source, Action removeAction) + private readonly IObservable> _source; + + public DisposeMany(IObservable> source, Action removeAction) { _source = source ?? throw new ArgumentNullException(nameof(source)); _removeAction = removeAction ?? throw new ArgumentNullException(nameof(removeAction)); @@ -23,44 +25,45 @@ public DisposeMany([NotNull] IObservable> source, Acti public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); - var cache = new Cache(); - var subscriber = _source - .Synchronize(locker) - .Do(changes => RegisterForRemoval(changes, cache), observer.OnError) - .SubscribeSafe(observer); + return Observable.Create>( + observer => + { + var locker = new object(); + var cache = new Cache(); + var subscriber = _source.Synchronize(locker).Do(changes => RegisterForRemoval(changes, cache), observer.OnError).SubscribeSafe(observer); - return Disposable.Create(() => - { - subscriber.Dispose(); + return Disposable.Create( + () => + { + subscriber.Dispose(); - lock (locker) - { - cache.Items.ForEach(t => _removeAction(t)); - cache.Clear(); - } - }); - }); + lock (locker) + { + cache.Items.ForEach(t => _removeAction(t)); + cache.Clear(); + } + }); + }); } private void RegisterForRemoval(IChangeSet changes, Cache cache) { - changes.ForEach(change => - { - switch (change.Reason) - { - case ChangeReason.Update: - // ReSharper disable once InconsistentlySynchronizedField - change.Previous.IfHasValue(t => _removeAction(t)); - break; - case ChangeReason.Remove: - // ReSharper disable once InconsistentlySynchronizedField - _removeAction(change.Current); - break; - } - }); + changes.ForEach( + change => + { + switch (change.Reason) + { + case ChangeReason.Update: + // ReSharper disable once InconsistentlySynchronizedField + change.Previous.IfHasValue(t => _removeAction(t)); + break; + + case ChangeReason.Remove: + // ReSharper disable once InconsistentlySynchronizedField + _removeAction(change.Current); + break; + } + }); cache.Clone(changes); } } diff --git a/src/DynamicData/Cache/Internal/DistinctCalculator.cs b/src/DynamicData/Cache/Internal/DistinctCalculator.cs index 21ea87430..e21a3bedb 100644 --- a/src/DynamicData/Cache/Internal/DistinctCalculator.cs +++ b/src/DynamicData/Cache/Internal/DistinctCalculator.cs @@ -1,21 +1,28 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class DistinctCalculator + where TValue : notnull + where TKey : notnull { + private readonly IDictionary _itemCache = new Dictionary(); + + private readonly IDictionary _keyCounters = new Dictionary(); + private readonly IObservable> _source; - private readonly Func _valueSelector; + private readonly IDictionary _valueCounters = new Dictionary(); - private readonly IDictionary _keyCounters = new Dictionary(); - private readonly IDictionary _itemCache = new Dictionary(); + + private readonly Func _valueSelector; public DistinctCalculator(IObservable> source, Func valueSelector) { @@ -32,21 +39,21 @@ private DistinctChangeSet Calculate(IChangeSet changes) { var result = new DistinctChangeSet(); - void AddKeyAction( TKey key, TValue value) => _keyCounters.Lookup(key) - .IfHasValue(count => _keyCounters[key] = count + 1) - .Else(() => - { - _keyCounters[key] = 1; - _itemCache[key] = value; // add to cache - }); + void AddKeyAction(TKey key, TValue value) => + _keyCounters.Lookup(key).IfHasValue(count => _keyCounters[key] = count + 1).Else( + () => + { + _keyCounters[key] = 1; + _itemCache[key] = value; // add to cache + }); - void AddValueAction( TValue value) => _valueCounters.Lookup(value) - .IfHasValue(count => _valueCounters[value] = count + 1) - .Else(() => - { - _valueCounters[value] = 1; - result.Add(new Change(ChangeReason.Add, value, value)); - }); + void AddValueAction(TValue value) => + _valueCounters.Lookup(value).IfHasValue(count => _valueCounters[value] = count + 1).Else( + () => + { + _valueCounters[value] = 1; + result.Add(new Change(ChangeReason.Add, value, value)); + }); void RemoveKeyAction(TKey key) { @@ -56,7 +63,7 @@ void RemoveKeyAction(TKey key) return; } - //decrement counter + // decrement counter var newCount = counter.Value - 1; _keyCounters[key] = newCount; if (newCount != 0) @@ -64,7 +71,7 @@ void RemoveKeyAction(TKey key) return; } - //if there are none, then remove from cache + // if there are none, then remove from cache _keyCounters.Remove(key); _itemCache.Remove(key); } @@ -77,7 +84,7 @@ void RemoveValueAction(TValue value) return; } - //decrement counter + // decrement counter var newCount = counter.Value - 1; _valueCounters[value] = newCount; if (newCount != 0) @@ -85,13 +92,12 @@ void RemoveValueAction(TValue value) return; } - //if there are none, then remove and notify + // if there are none, then remove and notify _valueCounters.Remove(value); result.Add(new Change(ChangeReason.Remove, value, value)); } - var enumerable = changes.ToConcreteType(); - foreach (var change in enumerable) + foreach (var change in changes.ToConcreteType()) { var key = change.Key; switch (change.Reason) @@ -103,6 +109,7 @@ void RemoveValueAction(TValue value) AddValueAction(value); break; } + case ChangeReason.Refresh: case ChangeReason.Update: { @@ -118,6 +125,7 @@ void RemoveValueAction(TValue value) _itemCache[key] = value; break; } + case ChangeReason.Remove: { var previous = _itemCache[key]; @@ -127,7 +135,8 @@ void RemoveValueAction(TValue value) } } } + return result; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/DynamicCombiner.cs b/src/DynamicData/Cache/Internal/DynamicCombiner.cs index fb064640a..968821039 100644 --- a/src/DynamicData/Cache/Internal/DynamicCombiner.cs +++ b/src/DynamicData/Cache/Internal/DynamicCombiner.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,18 +7,19 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class DynamicCombiner + where TKey : notnull { private readonly IObservableList>> _source; private readonly CombineOperator _type; - public DynamicCombiner([NotNull] IObservableList>> source, CombineOperator type) + public DynamicCombiner(IObservableList>> source, CombineOperator type) { _source = source ?? throw new ArgumentNullException(nameof(source)); _type = type; @@ -26,100 +27,119 @@ public DynamicCombiner([NotNull] IObservableList> Run() { - return Observable.Create>(observer => - { - var locker = new object(); - - //this is the resulting cache which produces all notifications - var resultCache = new ChangeAwareCache(); + return Observable.Create>( + observer => + { + var locker = new object(); + + // this is the resulting cache which produces all notifications + var resultCache = new ChangeAwareCache(); + + // Transform to a merge container. + // This populates a RefTracker when the original source is subscribed to + var sourceLists = _source.Connect().Synchronize(locker).Transform(changeSet => new MergeContainer(changeSet)).AsObservableList(); + + var sharedLists = sourceLists.Connect().Publish(); + + // merge the items back together + var allChanges = sharedLists.MergeMany(mc => mc.Source).Synchronize(locker).Subscribe( + changes => + { + // Populate result list and check for changes + UpdateResultList(resultCache, sourceLists.Items.AsArray(), changes); + + var notifications = resultCache.CaptureChanges(); + if (notifications.Count != 0) + { + observer.OnNext(notifications); + } + }); + + // when an list is removed, need to + var removedItem = sharedLists.OnItemRemoved( + mc => + { + // Remove items if required + ProcessChanges(resultCache, sourceLists.Items.AsArray(), mc.Cache.KeyValues); + + if (_type == CombineOperator.And || _type == CombineOperator.Except) + { + var itemsToCheck = sourceLists.Items.SelectMany(mc2 => mc2.Cache.KeyValues); + ProcessChanges(resultCache, sourceLists.Items.AsArray(), itemsToCheck); + } + + var notifications = resultCache.CaptureChanges(); + if (notifications.Count != 0) + { + observer.OnNext(notifications); + } + }).Subscribe(); + + // when an list is added or removed, need to + var sourceChanged = sharedLists.WhereReasonsAre(ListChangeReason.Add, ListChangeReason.AddRange).ForEachItemChange( + mc => + { + ProcessChanges(resultCache, sourceLists.Items.AsArray(), mc.Current.Cache.KeyValues); + + if (_type == CombineOperator.And || _type == CombineOperator.Except) + { + ProcessChanges(resultCache, sourceLists.Items.AsArray(), resultCache.KeyValues.ToArray()); + } + + var notifications = resultCache.CaptureChanges(); + if (notifications.Count != 0) + { + observer.OnNext(notifications); + } + }).Subscribe(); + + return new CompositeDisposable(sourceLists, allChanges, removedItem, sourceChanged, sharedLists.Connect()); + }); + } - //Transform to a merge container. - //This populates a RefTracker when the original source is subscribed to - var sourceLists = _source.Connect() - .Synchronize(locker) - .Transform(changeset => new MergeContainer(changeset)) - .AsObservableList(); + private bool MatchesConstraint(MergeContainer[] sources, TKey key) + { + if (sources.Length == 0) + { + return false; + } - var sharedLists = sourceLists.Connect().Publish(); + switch (_type) + { + case CombineOperator.And: + { + return sources.All(s => s.Cache.Lookup(key).HasValue); + } - //merge the items back together - var allChanges = sharedLists - .MergeMany(mc => mc.Source) - .Synchronize(locker) - .Subscribe(changes => + case CombineOperator.Or: { - //Populate result list and check for changes - UpdateResultList(resultCache, sourceLists.Items.AsArray(), changes); - - var notifications = resultCache.CaptureChanges(); - if (notifications.Count != 0) - { - observer.OnNext(notifications); - } - }); + return sources.Any(s => s.Cache.Lookup(key).HasValue); + } - //when an list is removed, need to - var removedItem = sharedLists - .OnItemRemoved(mc => + case CombineOperator.Xor: { - //Remove items if required - ProcessChanges(resultCache, sourceLists.Items.AsArray(), mc.Cache.KeyValues); - - if (_type == CombineOperator.And || _type == CombineOperator.Except) - { - var itemsToCheck = sourceLists.Items.SelectMany(mc2 => mc2.Cache.KeyValues); - ProcessChanges(resultCache, sourceLists.Items.AsArray(), itemsToCheck); - } - - var notifications = resultCache.CaptureChanges(); - if (notifications.Count != 0) - { - observer.OnNext(notifications); - } - }) - .Subscribe(); - - //when an list is added or removed, need to - var sourceChanged = sharedLists - .WhereReasonsAre(ListChangeReason.Add, ListChangeReason.AddRange) - .ForEachItemChange(mc => + return sources.Count(s => s.Cache.Lookup(key).HasValue) == 1; + } + + case CombineOperator.Except: { - ProcessChanges(resultCache, sourceLists.Items.AsArray(), mc.Current.Cache.KeyValues); - - if (_type == CombineOperator.And || _type == CombineOperator.Except) - { - ProcessChanges(resultCache, sourceLists.Items.AsArray(), resultCache.KeyValues.ToArray()); - } - - var notifications = resultCache.CaptureChanges(); - if (notifications.Count != 0) - { - observer.OnNext(notifications); - } - }) - .Subscribe(); - - return new CompositeDisposable(sourceLists, allChanges, removedItem, sourceChanged, sharedLists.Connect()); - }); - } + bool first = sources.Take(1).Any(s => s.Cache.Lookup(key).HasValue); + bool others = sources.Skip(1).Any(s => s.Cache.Lookup(key).HasValue); + return first && !others; + } - private void UpdateResultList(ChangeAwareCache target, MergeContainer[] sourceLists, IChangeSet changes) - { - foreach (var change in changes.ToConcreteType()) - { - ProcessItem(target, sourceLists, change.Current, change.Key); + default: + throw new ArgumentOutOfRangeException(nameof(key)); } } private void ProcessChanges(ChangeAwareCache target, MergeContainer[] sourceLists, IEnumerable> items) { - //check whether the item should be removed from the list (or in the case of And, added) - + // check whether the item should be removed from the list (or in the case of And, added) if (items is IList> list) { - //zero allocation enumerator - var enumerable = EnumerableIList.Create(list); - foreach (var item in enumerable) + // zero allocation enumerator + foreach (var item in EnumerableIList.Create(list)) { ProcessItem(target, sourceLists, item.Value, item.Key); } @@ -158,57 +178,29 @@ private void ProcessItem(ChangeAwareCache target, MergeContainer[ } } - private bool MatchesConstraint(MergeContainer[] sources, TKey key) + private void UpdateResultList(ChangeAwareCache target, MergeContainer[] sourceLists, IChangeSet changes) { - if (sources.Length == 0) - { - return false; - } - - switch (_type) + foreach (var change in changes.ToConcreteType()) { - case CombineOperator.And: - { - return sources.All(s => s.Cache.Lookup(key).HasValue); - } - - case CombineOperator.Or: - { - return sources.Any(s => s.Cache.Lookup(key).HasValue); - } - - case CombineOperator.Xor: - { - return sources.Count(s => s.Cache.Lookup(key).HasValue) == 1; - } - - case CombineOperator.Except: - { - bool first = sources.Take(1).Any(s => s.Cache.Lookup(key).HasValue); - bool others = sources.Skip(1).Any(s => s.Cache.Lookup(key).HasValue); - return first && !others; - } - - default: - throw new ArgumentOutOfRangeException(nameof(key)); + ProcessItem(target, sourceLists, change.Current, change.Key); } } private class MergeContainer { - public Cache Cache { get; } = new Cache(); - - public IObservable> Source { get; } - public MergeContainer(IObservable> source) { Source = source.Do(Clone); } + public Cache Cache { get; } = new(); + + public IObservable> Source { get; } + private void Clone(IChangeSet changes) { Cache.Clone(changes); } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/DynamicFilter.cs b/src/DynamicData/Cache/Internal/DynamicFilter.cs index f2e4bf584..dcb6f47a7 100644 --- a/src/DynamicData/Cache/Internal/DynamicFilter.cs +++ b/src/DynamicData/Cache/Internal/DynamicFilter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,14 +10,15 @@ namespace DynamicData.Cache.Internal { internal class DynamicFilter + where TKey : notnull { - private readonly IObservable> _source; private readonly IObservable> _predicateChanged; - private readonly IObservable _refilterObservable; - public DynamicFilter(IObservable> source, - IObservable> predicateChanged, - IObservable refilterObservable = null) + private readonly IObservable? _refilterObservable; + + private readonly IObservable> _source; + + public DynamicFilter(IObservable> source, IObservable> predicateChanged, IObservable? refilterObservable = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); _predicateChanged = predicateChanged ?? throw new ArgumentNullException(nameof(predicateChanged)); @@ -26,67 +27,62 @@ public DynamicFilter(IObservable> source, public IObservable> Run() { - return Observable.Create>(observer => - { - var allData = new Cache(); - var filteredData = new ChangeAwareCache(); - Func predicate = t => false; + return Observable.Create>( + observer => + { + var allData = new Cache(); + var filteredData = new ChangeAwareCache(); + Func predicate = _ => false; - var locker = new object(); + var locker = new object(); - var refresher = LatestPredicateObservable() - .Synchronize(locker) - .Select(p => - { - //set the local predicate - predicate = p; + var refresher = LatestPredicateObservable().Synchronize(locker).Select( + p => + { + // set the local predicate + predicate = p; - //reapply filter using all data from the cache - return filteredData.RefreshFilteredFrom(allData, predicate); - }); + // reapply filter using all data from the cache + return filteredData.RefreshFilteredFrom(allData, predicate); + }); - var dataChanged = _source - .Synchronize(locker) - .Select(changes => - { - //maintain all data [required to re-apply filter] - allData.Clone(changes); + var dataChanged = _source.Synchronize(locker).Select( + changes => + { + // maintain all data [required to re-apply filter] + allData.Clone(changes); - //maintain filtered data - filteredData.FilterChanges(changes, predicate); + // maintain filtered data + filteredData.FilterChanges(changes, predicate); - //get latest changes - return filteredData.CaptureChanges(); - }); + // get latest changes + return filteredData.CaptureChanges(); + }); - return refresher - .Merge(dataChanged) - .NotEmpty() - .SubscribeSafe(observer); - }); + return refresher.Merge(dataChanged).NotEmpty().SubscribeSafe(observer); + }); } private IObservable> LatestPredicateObservable() { - return Observable.Create>(observable => - { - Func latest = t => false; - - observable.OnNext(latest); - - var predicateChanged = _predicateChanged - .Subscribe(predicate => + return Observable.Create>( + observable => { - latest = predicate; + Func latest = _ => false; + observable.OnNext(latest); - }); - var reapplier = _refilterObservable == null - ? Disposable.Empty - : _refilterObservable.Subscribe(_ => observable.OnNext(latest)); + var predicateChanged = _predicateChanged.Subscribe( + predicate => + { + latest = predicate; + observable.OnNext(latest); + }); - return new CompositeDisposable(predicateChanged, reapplier); - }); + var reapplier = _refilterObservable is null ? Disposable.Empty : _refilterObservable.Subscribe(_ => observable.OnNext(latest)); + + return new CompositeDisposable(predicateChanged, reapplier); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/EditDiff.cs b/src/DynamicData/Cache/Internal/EditDiff.cs index 7cae4f6b6..0fd6ce539 100644 --- a/src/DynamicData/Cache/Internal/EditDiff.cs +++ b/src/DynamicData/Cache/Internal/EditDiff.cs @@ -1,22 +1,25 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class EditDiff + where TKey : notnull { - private readonly ISourceCache _source; private readonly Func _areEqual; + private readonly IEqualityComparer> _keyComparer = new KeyComparer(); - public EditDiff([NotNull] ISourceCache source, [NotNull] Func areEqual) + private readonly ISourceCache _source; + + public EditDiff(ISourceCache source, Func areEqual) { _source = source ?? throw new ArgumentNullException(nameof(source)); _areEqual = areEqual ?? throw new ArgumentNullException(nameof(areEqual)); @@ -24,24 +27,21 @@ public EditDiff([NotNull] ISourceCache source, [NotNull] Func items) { - _source.Edit(innerCache => - { - var originalItems = innerCache.KeyValues.AsArray(); - var newItems = innerCache.GetKeyValues(items).AsArray(); - - var removes = originalItems.Except(newItems, _keyComparer).ToArray(); - var adds = newItems.Except(originalItems, _keyComparer).ToArray(); - - //calculate intersect where the item has changed. - var intersect = newItems - .Select(kvp => new { Original = innerCache.Lookup(kvp.Key), NewItem = kvp }) - .Where(x => x.Original.HasValue && !_areEqual(x.Original.Value, x.NewItem.Value)) - .Select(x => new KeyValuePair(x.NewItem.Key, x.NewItem.Value)) - .ToArray(); - - innerCache.Remove(removes.Select(kvp => kvp.Key)); - innerCache.AddOrUpdate(adds.Union(intersect)); - }); + _source.Edit( + innerCache => + { + var originalItems = innerCache.KeyValues.AsArray(); + var newItems = innerCache.GetKeyValues(items).AsArray(); + + var removes = originalItems.Except(newItems, _keyComparer).ToArray(); + var adds = newItems.Except(originalItems, _keyComparer).ToArray(); + + // calculate intersect where the item has changed. + var intersect = newItems.Select(kvp => new { Original = innerCache.Lookup(kvp.Key), NewItem = kvp }).Where(x => x.Original.HasValue && !_areEqual(x.Original.Value, x.NewItem.Value)).Select(x => new KeyValuePair(x.NewItem.Key, x.NewItem.Value)).ToArray(); + + innerCache.Remove(removes.Select(kvp => kvp.Key)); + innerCache.AddOrUpdate(adds.Union(intersect)); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/ExpirableItem.cs b/src/DynamicData/Cache/Internal/ExpirableItem.cs index b28b7c1d8..a30345fa1 100644 --- a/src/DynamicData/Cache/Internal/ExpirableItem.cs +++ b/src/DynamicData/Cache/Internal/ExpirableItem.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,6 +9,14 @@ namespace DynamicData.Cache.Internal { internal readonly struct ExpirableItem : IEquatable> { + public ExpirableItem(TObject value, TKey key, DateTime dateTime, long index = 0) + { + Value = value; + Key = key; + ExpireAt = dateTime; + Index = index; + } + public TObject Value { get; } public TKey Key { get; } @@ -17,15 +25,15 @@ namespace DynamicData.Cache.Internal public long Index { get; } - public ExpirableItem(TObject value, TKey key, DateTime dateTime, long index = 0) + public static bool operator ==(ExpirableItem left, ExpirableItem right) { - Value = value; - Key = key; - ExpireAt = dateTime; - Index = index; + return left.Equals(right); } - #region Equality members + public static bool operator !=(ExpirableItem left, ExpirableItem right) + { + return !left.Equals(right); + } /// public bool Equals(ExpirableItem other) @@ -34,14 +42,9 @@ public bool Equals(ExpirableItem other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return obj is ExpirableItem && Equals((ExpirableItem) obj); + return obj is ExpirableItem value && Equals(value); } /// @@ -49,25 +52,13 @@ public override int GetHashCode() { unchecked { - return (EqualityComparer.Default.GetHashCode(Key) * 397) ^ ExpireAt.GetHashCode(); + return (Key is null ? 0 : EqualityComparer.Default.GetHashCode(Key) * 397) ^ ExpireAt.GetHashCode(); } } - public static bool operator ==(ExpirableItem left, ExpirableItem right) - { - return left.Equals(right); - } - - public static bool operator !=(ExpirableItem left, ExpirableItem right) - { - return !left.Equals(right); - } - - #endregion - public override string ToString() { return $"Key: {Key}, Expire At: {ExpireAt}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/FilterEx.cs b/src/DynamicData/Cache/Internal/FilterEx.cs index 6fa3aae32..b9f787125 100644 --- a/src/DynamicData/Cache/Internal/FilterEx.cs +++ b/src/DynamicData/Cache/Internal/FilterEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,65 +8,29 @@ namespace DynamicData.Cache.Internal { internal static class FilterEx { - public static IChangeSet RefreshFilteredFrom( - this ChangeAwareCache filtered, - Cache allData, - Func predicate) + public static void FilterChanges(this ChangeAwareCache cache, IChangeSet changes, Func predicate) + where TKey : notnull { - if (allData.Count == 0) - { - return ChangeSet.Empty; - } - - foreach (var kvp in allData.KeyValues) - { - var exisiting = filtered.Lookup(kvp.Key); - var matches = predicate(kvp.Value); - - if (matches) - { - if (!exisiting.HasValue) - { - filtered.Add(kvp.Value, kvp.Key); - } - } - else - { - if (exisiting.HasValue) - { - filtered.Remove(kvp.Key); - } - } - } - - return filtered.CaptureChanges(); - } - - public static void FilterChanges(this ChangeAwareCache cache, - IChangeSet changes, - Func predicate) - { - - var concreteType = changes.ToConcreteType(); - foreach (var change in concreteType) + foreach (var change in changes.ToConcreteType()) { var key = change.Key; switch (change.Reason) { case ChangeReason.Add: - { - var current = change.Current; - if (predicate(current)) + { + var current = change.Current; + if (predicate(current)) { cache.AddOrUpdate(current, key); } } break; + case ChangeReason.Update: - { - var current = change.Current; - if (predicate(current)) + { + var current = change.Current; + if (predicate(current)) { cache.AddOrUpdate(current, key); } @@ -77,15 +41,17 @@ public static void FilterChanges(this ChangeAwareCache(this ChangeAwareCache RefreshFilteredFrom(this ChangeAwareCache filtered, Cache allData, Func predicate) + where TKey : notnull + { + if (allData.Count == 0) + { + return ChangeSet.Empty; + } + + foreach (var kvp in allData.KeyValues) + { + var existing = filtered.Lookup(kvp.Key); + var matches = predicate(kvp.Value); + + if (matches) + { + if (!existing.HasValue) + { + filtered.Add(kvp.Value, kvp.Key); + } + } + else + { + if (existing.HasValue) + { + filtered.Remove(kvp.Key); + } + } + } + + return filtered.CaptureChanges(); + } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/FilterOnProperty.cs b/src/DynamicData/Cache/Internal/FilterOnProperty.cs index 61fae9a36..9a0cafb0b 100644 --- a/src/DynamicData/Cache/Internal/FilterOnProperty.cs +++ b/src/DynamicData/Cache/Internal/FilterOnProperty.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -11,19 +11,20 @@ namespace DynamicData.Cache.Internal { [Obsolete("Use AutoRefresh(), followed by Filter() instead")] internal class FilterOnProperty + where TKey : notnull where TObject : INotifyPropertyChanged { - private readonly IObservable> _source; - private readonly Expression> _propertySelector; private readonly Func _predicate; + + private readonly Expression> _propertySelector; + + private readonly IScheduler? _scheduler; + + private readonly IObservable> _source; + private readonly TimeSpan? _throttle; - private readonly IScheduler _scheduler; - public FilterOnProperty(IObservable> source, - Expression> propertySelector, - Func predicate, - TimeSpan? throttle = null, - IScheduler scheduler = null) + public FilterOnProperty(IObservable> source, Expression> propertySelector, Func predicate, TimeSpan? throttle = null, IScheduler? scheduler = null) { _source = source; _propertySelector = propertySelector; @@ -34,9 +35,7 @@ public FilterOnProperty(IObservable> source, public IObservable> Run() { - return _source - .AutoRefresh(_propertySelector, propertyChangeThrottle: _throttle, scheduler: _scheduler) - .Filter(_predicate); + return _source.AutoRefresh(_propertySelector, propertyChangeThrottle: _throttle, scheduler: _scheduler).Filter(_predicate); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/FilteredIndexCalculator.cs b/src/DynamicData/Cache/Internal/FilteredIndexCalculator.cs index e0ce9d3d3..ba00b0d70 100644 --- a/src/DynamicData/Cache/Internal/FilteredIndexCalculator.cs +++ b/src/DynamicData/Cache/Internal/FilteredIndexCalculator.cs @@ -1,26 +1,26 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; using System.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal static class FilteredIndexCalculator + where TKey : notnull { - public static IList> Calculate(IKeyValueCollection currentItems, - IKeyValueCollection previousItems, - IChangeSet sourceUpdates) + public static IList> Calculate(IKeyValueCollection currentItems, IKeyValueCollection previousItems, IChangeSet? sourceUpdates) { - if (currentItems.SortReason == SortReason.ComparerChanged || currentItems.SortReason== SortReason.InitialLoad) + if (currentItems.SortReason == SortReason.ComparerChanged || currentItems.SortReason == SortReason.InitialLoad) { - //clear collection and rebuild + // clear collection and rebuild var removed = previousItems.Select((item, index) => new Change(ChangeReason.Remove, item.Key, item.Value, index)); - var newitems = currentItems.Select((item, index) => new Change(ChangeReason.Add, item.Key, item.Value, index)); + var newItems = currentItems.Select((item, index) => new Change(ChangeReason.Add, item.Key, item.Value, index)); - return new List>(removed.Union(newitems)); + return new List>(removed.Union(newItems)); } var previousList = previousItems.ToList(); @@ -28,7 +28,7 @@ public static IList> Calculate(IKeyValueCollection(previousItems.Intersect(currentItems, keyComparer).Select(x => x.Key)); + var inBothKeys = new HashSet(previousItems.Intersect(currentItems, keyComparer).Select(x => x.Key)); var result = new List>(); foreach (var remove in removes) @@ -41,22 +41,16 @@ public static IList> Calculate(IKeyValueCollection(ChangeReason.Add, add.Key, add.Value, insertIndex)); } - //Adds and removes have been accounted for - //so check whether anything in the remaining change set have been moved ot updated - var remainingItems = sourceUpdates - .EmptyIfNull() - .Where(u => inbothKeys.Contains(u.Key) - && (u.Reason == ChangeReason.Update - || u.Reason == ChangeReason.Moved - || u.Reason == ChangeReason.Refresh)) - .ToList(); + // Adds and removes have been accounted for + // so check whether anything in the remaining change set have been moved ot updated + var remainingItems = sourceUpdates.EmptyIfNull().Where(u => inBothKeys.Contains(u.Key) && (u.Reason == ChangeReason.Update || u.Reason == ChangeReason.Moved || u.Reason == ChangeReason.Refresh)).ToList(); foreach (var change in remainingItems) { @@ -65,27 +59,26 @@ public static IList> Calculate(IKeyValueCollection(change.Key, change.Current); var previous = new KeyValuePair(change.Key, change.Previous.Value); - //remove from the actual index + // remove from the actual index var removeIndex = previousList.IndexOf(previous); previousList.RemoveAt(removeIndex); - //insert into the desired index + // insert into the desired index int desiredIndex = previousList.BinarySearch(current, currentItems.Comparer); int insertIndex = ~desiredIndex; previousList.Insert(insertIndex, current); - result.Add(new Change(ChangeReason.Update, current.Key, current.Value, - previous.Value, insertIndex, removeIndex)); + result.Add(new Change(ChangeReason.Update, current.Key, current.Value, previous.Value, insertIndex, removeIndex)); } else if (change.Reason == ChangeReason.Moved) { - //TODO: We have the index already, would be more efficient to calculate new position from the original index + // TODO: We have the index already, would be more efficient to calculate new position from the original index var current = new KeyValuePair(change.Key, change.Current); - var previousindex = previousList.IndexOf(current); + var previousIndex = previousList.IndexOf(current); int desiredIndex = currentItems.IndexOf(current); - if (previousindex == desiredIndex) + if (previousIndex == desiredIndex) { continue; } @@ -95,24 +88,22 @@ public static IList> Calculate(IKeyValueCollection(current.Key, current.Value, desiredIndex, previousindex)); + result.Add(new Change(current.Key, current.Value, desiredIndex, previousIndex)); } else { - //TODO: re-evaluate to check whether item should be moved + // TODO: re-evaluate to check whether item should be moved result.Add(change); } } - //Alternative to evaluate is to check order - var evaluates = remainingItems.Where(c => c.Reason == ChangeReason.Refresh) - .OrderByDescending(x => new KeyValuePair(x.Key, x.Current), currentItems.Comparer) - .ToList(); + // Alternative to evaluate is to check order + var evaluates = remainingItems.Where(c => c.Reason == ChangeReason.Refresh).OrderByDescending(x => new KeyValuePair(x.Key, x.Current), currentItems.Comparer).ToList(); - //calculate moves. Very expensive operation - //TODO: Try and make this better + // calculate moves. Very expensive operation + // TODO: Try and make this better foreach (var u in evaluates) { var current = new KeyValuePair(u.Key, u.Current); @@ -123,28 +114,27 @@ public static IList> Calculate(IKeyValueCollection(u.Key, u.Current, newposition, old)); + previousList.Insert(newPosition, current); + result.Add(new Change(u.Key, u.Current, newPosition, old)); } return result; } - private static int GetInsertPositionLinear(IList> list, KeyValuePair item, - IComparer> comparer) + private static int GetInsertPositionLinear(IList> list, KeyValuePair item, IComparer> comparer) { for (var i = 0; i < list.Count; i++) { @@ -157,4 +147,4 @@ private static int GetInsertPositionLinear(IList> li return list.Count; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/FinallySafe.cs b/src/DynamicData/Cache/Internal/FinallySafe.cs index 5d0f8f566..c920b2891 100644 --- a/src/DynamicData/Cache/Internal/FinallySafe.cs +++ b/src/DynamicData/Cache/Internal/FinallySafe.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.Cache.Internal { internal class FinallySafe { - private readonly IObservable _source; private readonly Action _finallyAction; - public FinallySafe([NotNull] IObservable source, [NotNull] Action finallyAction) + private readonly IObservable _source; + + public FinallySafe(IObservable source, Action finallyAction) { _source = source ?? throw new ArgumentNullException(nameof(source)); _finallyAction = finallyAction ?? throw new ArgumentNullException(nameof(finallyAction)); @@ -22,36 +22,38 @@ public FinallySafe([NotNull] IObservable source, [NotNull] Action finallyActi public IObservable Run() { - return Observable.Create(o => - { - var finallyOnce = Disposable.Create(_finallyAction); - - var subscription = _source.Subscribe(o.OnNext, - ex => + return Observable.Create( + o => { - try - { - o.OnError(ex); - } - finally - { - finallyOnce.Dispose(); - } - }, - () => - { - try - { - o.OnCompleted(); - } - finally - { - finallyOnce.Dispose(); - } - }); + var finallyOnce = Disposable.Create(_finallyAction); - return new CompositeDisposable(subscription, finallyOnce); - }); + var subscription = _source.Subscribe( + o.OnNext, + ex => + { + try + { + o.OnError(ex); + } + finally + { + finallyOnce.Dispose(); + } + }, + () => + { + try + { + o.OnCompleted(); + } + finally + { + finallyOnce.Dispose(); + } + }); + + return new CompositeDisposable(subscription, finallyOnce); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/FullJoin.cs b/src/DynamicData/Cache/Internal/FullJoin.cs index 2054434e7..8d7b029f4 100644 --- a/src/DynamicData/Cache/Internal/FullJoin.cs +++ b/src/DynamicData/Cache/Internal/FullJoin.cs @@ -1,25 +1,28 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class FullJoin + where TLeftKey : notnull + where TRightKey : notnull { private readonly IObservable> _left; + + private readonly Func, Optional, TDestination> _resultSelector; + private readonly IObservable> _right; + private readonly Func _rightKeySelector; - private readonly Func, Optional, TDestination> _resultSelector; - public FullJoin(IObservable> left, - IObservable> right, - Func rightKeySelector, - Func, Optional, TDestination> resultSelector) + public FullJoin(IObservable> left, IObservable> right, Func rightKeySelector, Func, Optional, TDestination> resultSelector) { _left = left ?? throw new ArgumentNullException(nameof(left)); _right = right ?? throw new ArgumentNullException(nameof(right)); @@ -29,107 +32,108 @@ public FullJoin(IObservable> left, public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); + return Observable.Create>( + observer => + { + var locker = new object(); - //create local backing stores - var leftCache = _left.Synchronize(locker).AsObservableCache(false); - var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(false); + // create local backing stores + var leftCache = _left.Synchronize(locker).AsObservableCache(false); + var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(false); - //joined is the final cache - var joinedCache = new LockFreeObservableCache(); + // joined is the final cache + var joinedCache = new LockFreeObservableCache(); - var leftLoader = leftCache.Connect() - .Subscribe(changes => - { - joinedCache.Edit(innerCache => - { - foreach (var change in changes.ToConcreteType()) - { - var left = change.Current; - var right = rightCache.Lookup(change.Key); - - switch (change.Reason) + var leftLoader = leftCache.Connect().Subscribe( + changes => { - case ChangeReason.Add: - case ChangeReason.Update: - innerCache.AddOrUpdate(_resultSelector(change.Key, left, right), change.Key); - break; - case ChangeReason.Remove: - - if (!right.HasValue) - { - //remove from result because there is no left and no rights - innerCache.Remove(change.Key); - } - else - { - //update with no left value - innerCache.AddOrUpdate(_resultSelector(change.Key, Optional.None, right), change.Key); - } - - break; - case ChangeReason.Refresh: - //propagate upstream - innerCache.Refresh(change.Key); - break; - } - } - }); - }); + joinedCache.Edit( + innerCache => + { + foreach (var change in changes.ToConcreteType()) + { + var left = change.Current; + var right = rightCache.Lookup(change.Key); - var rightLoader = rightCache.Connect() - .Subscribe(changes => - { - joinedCache.Edit(innerCache => - { - foreach (var change in changes.ToConcreteType()) - { - var right = change.Current; - var left = leftCache.Lookup(change.Key); - - switch (change.Reason) + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + innerCache.AddOrUpdate(_resultSelector(change.Key, left, right), change.Key); + break; + + case ChangeReason.Remove: + + if (!right.HasValue) + { + // remove from result because there is no left and no rights + innerCache.Remove(change.Key); + } + else + { + // update with no left value + innerCache.AddOrUpdate(_resultSelector(change.Key, Optional.None, right), change.Key); + } + + break; + + case ChangeReason.Refresh: + // propagate upstream + innerCache.Refresh(change.Key); + break; + } + } + }); + }); + + var rightLoader = rightCache.Connect().Subscribe( + changes => { - case ChangeReason.Add: - case ChangeReason.Update: - { - innerCache.AddOrUpdate(_resultSelector(change.Key, left, right), change.Key); - } - - break; - case ChangeReason.Remove: - { - if (!left.HasValue) - { - //remove from result because there is no left and no rights - innerCache.Remove(change.Key); - } - else + joinedCache.Edit( + innerCache => { - //update with no right value - innerCache.AddOrUpdate(_resultSelector(change.Key, left, Optional.None), change.Key); - } - } - - break; - case ChangeReason.Refresh: - //propagate upstream - innerCache.Refresh(change.Key); - break; - } - } - }); - }); + foreach (var change in changes.ToConcreteType()) + { + var right = change.Current; + var left = leftCache.Lookup(change.Key); - return new CompositeDisposable( - joinedCache.Connect().NotEmpty().SubscribeSafe(observer), - leftCache, - rightCache, - leftLoader, - joinedCache, - rightLoader); - }); + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + { + innerCache.AddOrUpdate(_resultSelector(change.Key, left, right), change.Key); + } + + break; + + case ChangeReason.Remove: + { + if (!left.HasValue) + { + // remove from result because there is no left and no rights + innerCache.Remove(change.Key); + } + else + { + // update with no right value + innerCache.AddOrUpdate(_resultSelector(change.Key, left, Optional.None), change.Key); + } + } + + break; + + case ChangeReason.Refresh: + // propagate upstream + innerCache.Refresh(change.Key); + break; + } + } + }); + }); + + return new CompositeDisposable(joinedCache.Connect().NotEmpty().SubscribeSafe(observer), leftCache, rightCache, leftLoader, joinedCache, rightLoader); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/FullJoinMany.cs b/src/DynamicData/Cache/Internal/FullJoinMany.cs index 5f0d997eb..1927a5c1f 100644 --- a/src/DynamicData/Cache/Internal/FullJoinMany.cs +++ b/src/DynamicData/Cache/Internal/FullJoinMany.cs @@ -1,24 +1,26 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class FullJoinMany + where TLeftKey : notnull + where TRightKey : notnull { private readonly IObservable> _left; + + private readonly Func, IGrouping, TDestination> _resultSelector; + private readonly IObservable> _right; + private readonly Func _rightKeySelector; - private readonly Func, IGrouping, TDestination> _resultSelector; - public FullJoinMany([NotNull] IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, IGrouping, TDestination> resultSelector) + public FullJoinMany(IObservable> left, IObservable> right, Func rightKeySelector, Func, IGrouping, TDestination> resultSelector) { _left = left ?? throw new ArgumentNullException(nameof(left)); _right = right ?? throw new ArgumentNullException(nameof(right)); @@ -31,11 +33,7 @@ public IObservable> Run() var emptyCache = Cache.Empty; var rightGrouped = _right.GroupWithImmutableState(_rightKeySelector); - return _left.FullJoin(rightGrouped, grouping => grouping.Key, - (leftKey, left, grouping) => _resultSelector(leftKey, left, grouping.ValueOr(() => - { - return new ImmutableGroup(leftKey, emptyCache); - }))); + return _left.FullJoin(rightGrouped, grouping => grouping.Key, (leftKey, left, grouping) => _resultSelector(leftKey, left, grouping.ValueOr(() => new ImmutableGroup(leftKey, emptyCache)))); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/GroupImmutable.cs b/src/DynamicData/Cache/Internal/GroupImmutable.cs deleted file mode 100644 index a28023eb3..000000000 --- a/src/DynamicData/Cache/Internal/GroupImmutable.cs +++ /dev/null @@ -1,320 +0,0 @@ -// Copyright (c) 2011-2019 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. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using DynamicData.Kernel; - -namespace DynamicData.Cache.Internal -{ - internal sealed class GroupOnImmutable - { - private readonly IObservable> _source; - private readonly Func _groupSelectorKey; - private readonly IObservable _regrouper; - - public GroupOnImmutable(IObservable> source, Func groupSelectorKey, IObservable regrouper) - { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _groupSelectorKey = groupSelectorKey ?? throw new ArgumentNullException(nameof(groupSelectorKey)); - _regrouper = regrouper ?? Observable.Never(); - } - - public IObservable> Run() - { - return Observable.Create> - ( - observer => - { - var locker = new object(); - var grouper = new Grouper(_groupSelectorKey); - - var groups = _source - .Synchronize(locker) - .Select(grouper.Update) - .Where(changes => changes.Count != 0); - - var regroup = _regrouper.Synchronize(locker) - .Select(_ => grouper.Regroup()) - .Where(changes => changes.Count != 0); - - return groups.Merge(regroup).SubscribeSafe(observer); - }); - } - - private sealed class Grouper - { - private readonly IDictionary _allGroupings = new Dictionary(); - private readonly Func _groupSelectorKey; - private readonly IDictionary _itemCache = new Dictionary(); - - public Grouper(Func groupSelectorKey) - { - _groupSelectorKey = groupSelectorKey; - } - - public IImmutableGroupChangeSet Update(IChangeSet updates) - { - return HandleUpdates(updates); - } - - public IImmutableGroupChangeSet Regroup() - { - //re-evaluate all items in the group - var items = _itemCache.Select(item => new Change(ChangeReason.Refresh, item.Key, item.Value.Item)); - return HandleUpdates(new ChangeSet(items)); - } - - private IImmutableGroupChangeSet HandleUpdates(IEnumerable> changes) - { - //need to keep track of effected groups to calculate correct notifications - var initialStateOfGroups = new Dictionary>(); - - //1. Group all items - var grouped = changes - .Select(u => new ChangeWithGroup(u, _groupSelectorKey)) - .GroupBy(c => c.GroupKey); - - //2. iterate and maintain child caches - grouped.ForEach(group => - { - var groupItem = GetCache(group.Key); - var groupCache = groupItem.Item1; - var cacheToModify = groupCache.Cache; - - if (!initialStateOfGroups.ContainsKey(group.Key)) - { - initialStateOfGroups[group.Key] = GetGroupState(groupCache); - } - - //1. Iterate through group changes and maintain the current group - foreach (var current in group) - { - switch (current.Reason) - { - case ChangeReason.Add: - { - cacheToModify.AddOrUpdate(current.Item, current.Key); - _itemCache[current.Key] = current; - break; - } - - case ChangeReason.Update: - { - cacheToModify.AddOrUpdate(current.Item, current.Key); - - //check whether the previous item was in a different group. If so remove from old group - var previous = _itemCache.Lookup(current.Key) - .ValueOrThrow(() => CreateMissingKeyException(ChangeReason.Update, current.Key)); - - if (!previous.GroupKey.Equals(current.GroupKey)) - { - RemoveFromOldGroup(initialStateOfGroups, previous.GroupKey, current.Key); - } - - _itemCache[current.Key] = current; - break; - } - - case ChangeReason.Remove: - { - var existing = cacheToModify.Lookup(current.Key); - if (existing.HasValue) - { - cacheToModify.Remove(current.Key); - } - else - { - //this has been removed due to an underlying evaluate resulting in a remove - var previousGroupKey = _itemCache.Lookup(current.Key) - .ValueOrThrow(() => CreateMissingKeyException(ChangeReason.Remove, current.Key)) - .GroupKey; - - RemoveFromOldGroup(initialStateOfGroups, previousGroupKey, current.Key); - } - - _itemCache.Remove(current.Key); - - break; - } - - case ChangeReason.Refresh: - { - //check whether the previous item was in a different group. If so remove from old group - var previous = _itemCache.Lookup(current.Key); - - previous.IfHasValue(p => - { - if (p.GroupKey.Equals(current.GroupKey)) - { - return; - } - - RemoveFromOldGroup(initialStateOfGroups, p.GroupKey, current.Key); - //add to new group because the group value has changed - cacheToModify.AddOrUpdate(current.Item, current.Key); - }).Else(() => - { - //must be created due to addition - cacheToModify.AddOrUpdate(current.Item, current.Key); - }); - - _itemCache[current.Key] = current; - break; - } - } - } - }); - - //2. Produce and fire notifications [compare current and previous state] - return CreateChangeSet(initialStateOfGroups); - } - - private static Exception CreateMissingKeyException(ChangeReason reason, TKey key) - { - var message = $"{key} is missing from previous group on {reason}." + - $"{Environment.NewLine}Object type {typeof(TObject)}, Key type {typeof(TKey)}, Group key type {typeof(TGroupKey)}"; - return new MissingKeyException(message); - } - - private void RemoveFromOldGroup(IDictionary> groupState, TGroupKey groupKey, TKey currentKey) - { - _allGroupings.Lookup(groupKey) - .IfHasValue(g => - { - if (!groupState.ContainsKey(g.Key)) - { - groupState[g.Key] = GetGroupState(g.Key, g.Cache); - } - - g.Cache.Remove(currentKey); - }); - } - - private IImmutableGroupChangeSet CreateChangeSet(IDictionary> initialGroupState) - { - var result = new List, TGroupKey>>(); - foreach (var intialGroup in initialGroupState) - { - var key = intialGroup.Key; - var current = _allGroupings[intialGroup.Key]; - - if (current.Cache.Count == 0) - { - _allGroupings.Remove(key); - result.Add(new Change, TGroupKey>(ChangeReason.Remove, key, intialGroup.Value)); - } - else - { - var currentState = GetGroupState(current); - if (intialGroup.Value.Count == 0) - { - result.Add(new Change, TGroupKey>(ChangeReason.Add, key, currentState)); - } - else - { - var previousState = Optional.Some(intialGroup.Value); - result.Add(new Change, TGroupKey>(ChangeReason.Update, key, currentState, previousState)); - } - } - } - - return new ImmutableGroupChangeSet(result); - } - - private static IGrouping GetGroupState(GroupCache grouping) - { - return new ImmutableGroup(grouping.Key, grouping.Cache); - } - - private static IGrouping GetGroupState(TGroupKey key, ICache cache) - { - return new ImmutableGroup(key, cache); - } - - private class GroupCache - { - public TGroupKey Key { get; } - public Cache Cache { get; } - - public GroupCache(TGroupKey key) - { - Key = key; - Cache = new Cache(); - } - } - - private Tuple GetCache(TGroupKey key) - { - var cache = _allGroupings.Lookup(key); - if (cache.HasValue) - { - return Tuple.Create(cache.Value, false); - } - - var newcache = new GroupCache(key); - _allGroupings[key] = newcache; - return Tuple.Create(newcache, true); - } - - private struct ChangeWithGroup : IEquatable - { - public ChangeWithGroup(Change change, Func keySelector) - { - GroupKey = keySelector(change.Current); - Item = change.Current; - Key = change.Key; - Reason = change.Reason; - } - - public TObject Item { get; } - public TKey Key { get; } - public TGroupKey GroupKey { get; } - public ChangeReason Reason { get; } - - #region Equality members - - public bool Equals(ChangeWithGroup other) - { - return Key.Equals(other.Key); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return obj is ChangeWithGroup && Equals((ChangeWithGroup)obj); - } - - public override int GetHashCode() - { - return Key.GetHashCode(); - } - - public static bool operator ==(ChangeWithGroup left, ChangeWithGroup right) - { - return left.Equals(right); - } - - public static bool operator !=(ChangeWithGroup left, ChangeWithGroup right) - { - return !left.Equals(right); - } - - #endregion - - public override string ToString() - { - return $"Key: {Key}, GroupKey: {GroupKey}, Item: {Item}"; - } - } - } - } -} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/GroupOn.cs b/src/DynamicData/Cache/Internal/GroupOn.cs index 7f8fbca38..6f634ace4 100644 --- a/src/DynamicData/Cache/Internal/GroupOn.cs +++ b/src/DynamicData/Cache/Internal/GroupOn.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,17 +8,22 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class GroupOn + where TKey : notnull + where TGroupKey : notnull { - private readonly IObservable> _source; private readonly Func _groupSelectorKey; + private readonly IObservable _regrouper; - public GroupOn(IObservable> source, Func groupSelectorKey, IObservable regrouper) + private readonly IObservable> _source; + + public GroupOn(IObservable> source, Func groupSelectorKey, IObservable? regrouper) { _source = source ?? throw new ArgumentNullException(nameof(source)); _groupSelectorKey = groupSelectorKey ?? throw new ArgumentNullException(nameof(groupSelectorKey)); @@ -27,22 +32,15 @@ public GroupOn(IObservable> source, Func> Run() { - return Observable.Create> - ( - observer => + return Observable.Create>( + observer => { var locker = new object(); var grouper = new Grouper(_groupSelectorKey); - var groups = _source - .Finally(observer.OnCompleted) - .Synchronize(locker) - .Select(grouper.Update) - .Where(changes => changes.Count != 0); + var groups = _source.Finally(observer.OnCompleted).Synchronize(locker).Select(grouper.Update).Where(changes => changes.Count != 0); - var regroup = _regrouper.Synchronize(locker) - .Select(_ => grouper.Regroup()) - .Where(changes => changes.Count != 0); + var regroup = _regrouper.Synchronize(locker).Select(_ => grouper.Regroup()).Where(changes => changes.Count != 0); var published = groups.Merge(regroup).Publish(); var subscriber = published.SubscribeSafe(observer); @@ -50,19 +48,22 @@ public IObservable> Run() var connected = published.Connect(); - return Disposable.Create(() => - { - connected.Dispose(); - disposer.Dispose(); - subscriber.Dispose(); - }); + return Disposable.Create( + () => + { + connected.Dispose(); + disposer.Dispose(); + subscriber.Dispose(); + }); }); } private sealed class Grouper { private readonly IDictionary> _groupCache = new Dictionary>(); + private readonly Func _groupSelectorKey; + private readonly IDictionary _itemCache = new Dictionary(); public Grouper(Func groupSelectorKey) @@ -70,184 +71,182 @@ public Grouper(Func groupSelectorKey) _groupSelectorKey = groupSelectorKey; } + public IGroupChangeSet Regroup() + { + // re-evaluate all items in the group + var items = _itemCache.Select(item => new Change(ChangeReason.Refresh, item.Key, item.Value.Item)); + return HandleUpdates(new ChangeSet(items), true); + } + public IGroupChangeSet Update(IChangeSet updates) { return HandleUpdates(updates); } - public IGroupChangeSet Regroup() + private Tuple, bool> GetCache(TGroupKey key) { - //re-evaluate all items in the group - var items = _itemCache.Select(item => new Change(ChangeReason.Refresh, item.Key, item.Value.Item)); - return HandleUpdates(new ChangeSet(items), true); + var cache = _groupCache.Lookup(key); + if (cache.HasValue) + { + return Tuple.Create(cache.Value, false); + } + + var newcache = new ManagedGroup(key); + _groupCache[key] = newcache; + return Tuple.Create(newcache, true); } private GroupChangeSet HandleUpdates(IEnumerable> changes, bool isRegrouping = false) { var result = new List, TGroupKey>>(); - //Group all items - var grouped = changes - .Select(u => new ChangeWithGroup(u, _groupSelectorKey)) - .GroupBy(c => c.GroupKey); + // Group all items + var grouped = changes.Select(u => new ChangeWithGroup(u, _groupSelectorKey)).GroupBy(c => c.GroupKey); - //1. iterate and maintain child caches (_groupCache) - //2. maintain which group each item belongs to (_itemCache) - grouped.ForEach(group => - { - var groupItem = GetCache(group.Key); - var groupCache = groupItem.Item1; - if (groupItem.Item2) - { - result.Add(new Change, TGroupKey>(ChangeReason.Add, group.Key, groupCache)); - } - - groupCache.Update(groupUpdater => - { - foreach (var current in group) + // 1. iterate and maintain child caches (_groupCache) + // 2. maintain which group each item belongs to (_itemCache) + grouped.ForEach( + group => { - switch (current.Reason) + var groupItem = GetCache(group.Key); + var groupCache = groupItem.Item1; + if (groupItem.Item2) { - case ChangeReason.Add: - { - groupUpdater.AddOrUpdate(current.Item, current.Key); - _itemCache[current.Key] = current; - break; - } + result.Add(new Change, TGroupKey>(ChangeReason.Add, group.Key, groupCache)); + } - case ChangeReason.Update: + groupCache.Update( + groupUpdater => { - groupUpdater.AddOrUpdate(current.Item, current.Key); - - //check whether the previous item was in a different group. If so remove from old group - var previous = _itemCache.Lookup(current.Key) - .ValueOrThrow(() => new MissingKeyException($"{current.Key} is missing from previous value on update. Object type {typeof(TObject).FullName}, Key type {typeof(TKey).FullName}, Group key type {typeof(TGroupKey).FullName}")); - - if (!EqualityComparer.Default.Equals(previous.GroupKey,current.GroupKey)) + foreach (var current in group) { - _groupCache.Lookup(previous.GroupKey) - .IfHasValue(g => - { - g.Update(u => u.Remove(current.Key)); - if (g.Count != 0) + switch (current.Reason) + { + case ChangeReason.Add: { - return; + groupUpdater.AddOrUpdate(current.Item, current.Key); + _itemCache[current.Key] = current; + break; } - _groupCache.Remove(g.Key); - result.Add(new Change, TGroupKey>(ChangeReason.Remove, g.Key, g)); - }); - - _itemCache[current.Key] = current; - } - - break; - } - - case ChangeReason.Remove: - { - var previousInSameGroup = groupUpdater.Lookup(current.Key); - if (previousInSameGroup.HasValue) - { - groupUpdater.Remove(current.Key); - } - else - { - //this has been removed due to an underlying evaluate resulting in a remove - var previousGroupKey = _itemCache.Lookup(current.Key) - .ValueOrThrow(() => new MissingKeyException($"{current.Key} is missing from previous value on remove. Object type {typeof(TObject).FullName}, Key type {typeof(TKey).FullName}, Group key type {typeof(TGroupKey).FullName}")) - .GroupKey; - - _groupCache.Lookup(previousGroupKey) - .IfHasValue(g => - { - g.Update(u => u.Remove(current.Key)); - if (g.Count != 0) - { - return; - } - - _groupCache.Remove(g.Key); - result.Add(new Change, TGroupKey>(ChangeReason.Remove, g.Key, g)); - }); - } - - //finally, remove the current item from the item cache - _itemCache.Remove(current.Key); - - break; - } - - case ChangeReason.Refresh: - { - //check whether the previous item was in a different group. If so remove from old group - var previous = _itemCache.Lookup(current.Key); - - previous.IfHasValue(p => - { + case ChangeReason.Update: + { + groupUpdater.AddOrUpdate(current.Item, current.Key); + + // check whether the previous item was in a different group. If so remove from old group + var previous = _itemCache.Lookup(current.Key).ValueOrThrow(() => new MissingKeyException($"{current.Key} is missing from previous value on update. Object type {typeof(TObject).FullName}, Key type {typeof(TKey).FullName}, Group key type {typeof(TGroupKey).FullName}")); + + if (!EqualityComparer.Default.Equals(previous.GroupKey, current.GroupKey)) + { + _groupCache.Lookup(previous.GroupKey).IfHasValue( + g => + { + g.Update(u => u.Remove(current.Key)); + if (g.Count != 0) + { + return; + } + + _groupCache.Remove(g.Key); + result.Add(new Change, TGroupKey>(ChangeReason.Remove, g.Key, g)); + }); + + _itemCache[current.Key] = current; + } + + break; + } - if (EqualityComparer.Default.Equals(p.GroupKey,current.GroupKey)) - { - //propagate evaluates up the chain - if (!isRegrouping) - { - groupUpdater.Refresh(current.Key); - } + case ChangeReason.Remove: + { + var previousInSameGroup = groupUpdater.Lookup(current.Key); + if (previousInSameGroup.HasValue) + { + groupUpdater.Remove(current.Key); + } + else + { + // this has been removed due to an underlying evaluate resulting in a remove + var previousGroupKey = _itemCache.Lookup(current.Key).ValueOrThrow(() => new MissingKeyException($"{current.Key} is missing from previous value on remove. Object type {typeof(TObject).FullName}, Key type {typeof(TKey).FullName}, Group key type {typeof(TGroupKey).FullName}")).GroupKey; + + _groupCache.Lookup(previousGroupKey).IfHasValue( + g => + { + g.Update(u => u.Remove(current.Key)); + if (g.Count != 0) + { + return; + } + + _groupCache.Remove(g.Key); + result.Add(new Change, TGroupKey>(ChangeReason.Remove, g.Key, g)); + }); + } + + // finally, remove the current item from the item cache + _itemCache.Remove(current.Key); + + break; + } - return; + case ChangeReason.Refresh: + { + // check whether the previous item was in a different group. If so remove from old group + var previous = _itemCache.Lookup(current.Key); + + previous.IfHasValue( + p => + { + if (EqualityComparer.Default.Equals(p.GroupKey, current.GroupKey)) + { + // propagate evaluates up the chain + if (!isRegrouping) + { + groupUpdater.Refresh(current.Key); + } + + return; + } + + _groupCache.Lookup(p.GroupKey).IfHasValue( + g => + { + g.Update(u => u.Remove(current.Key)); + if (g.Count != 0) + { + return; + } + + _groupCache.Remove(g.Key); + result.Add(new Change, TGroupKey>(ChangeReason.Remove, g.Key, g)); + }); + + groupUpdater.AddOrUpdate(current.Item, current.Key); + }).Else( + () => + { + // must be created due to addition + groupUpdater.AddOrUpdate(current.Item, current.Key); + }); + + _itemCache[current.Key] = current; + + break; + } } + } + }); - _groupCache.Lookup(p.GroupKey) - .IfHasValue(g => - { - g.Update(u => u.Remove(current.Key)); - if (g.Count != 0) - { - return; - } - - _groupCache.Remove(g.Key); - result.Add(new Change, TGroupKey>(ChangeReason.Remove, g.Key, g)); - }); - - groupUpdater.AddOrUpdate(current.Item, current.Key); - }).Else(() => - { - //must be created due to addition - groupUpdater.AddOrUpdate(current.Item, current.Key); - }); - - _itemCache[current.Key] = current; - - break; - } + if (groupCache.Count == 0) + { + _groupCache.RemoveIfContained(group.Key); + result.Add(new Change, TGroupKey>(ChangeReason.Remove, group.Key, groupCache)); } - } - }); - - if (groupCache.Count == 0) - { - _groupCache.RemoveIfContained(group.Key); - result.Add(new Change, TGroupKey>(ChangeReason.Remove, @group.Key, groupCache)); - } - }); + }); return new GroupChangeSet(result); } - private Tuple, bool> GetCache(TGroupKey key) - { - var cache = _groupCache.Lookup(key); - if (cache.HasValue) - { - return Tuple.Create(cache.Value, false); - } - - var newcache = new ManagedGroup(key); - _groupCache[key] = newcache; - return Tuple.Create(newcache, true); - } - private readonly struct ChangeWithGroup : IEquatable { public ChangeWithGroup(Change change, Func keySelector) @@ -266,40 +265,31 @@ public ChangeWithGroup(Change change, Func ke public ChangeReason Reason { get; } - #region Equality members - - public bool Equals(ChangeWithGroup other) + public static bool operator ==(ChangeWithGroup left, ChangeWithGroup right) { - return EqualityComparer.Default.Equals(Key, other.Key); + return left.Equals(right); } - public override bool Equals(object obj) + public static bool operator !=(ChangeWithGroup left, ChangeWithGroup right) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return obj is ChangeWithGroup @group && Equals(@group); + return !left.Equals(right); } - public override int GetHashCode() + public bool Equals(ChangeWithGroup other) { - return Key.GetHashCode(); + return EqualityComparer.Default.Equals(Key, other.Key); } - public static bool operator ==(ChangeWithGroup left, ChangeWithGroup right) + public override bool Equals(object? obj) { - return left.Equals(right); + return obj is ChangeWithGroup group && Equals(group); } - public static bool operator !=(ChangeWithGroup left, ChangeWithGroup right) + public override int GetHashCode() { - return !left.Equals(right); + return Key.GetHashCode(); } - #endregion - public override string ToString() { return $"Key: {Key}, GroupKey: {GroupKey}, Item: {Item}"; @@ -307,4 +297,4 @@ public override string ToString() } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/GroupOnImmutable.cs b/src/DynamicData/Cache/Internal/GroupOnImmutable.cs new file mode 100644 index 000000000..f6aa7dc29 --- /dev/null +++ b/src/DynamicData/Cache/Internal/GroupOnImmutable.cs @@ -0,0 +1,314 @@ +// Copyright (c) 2011-2020 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; + +using DynamicData.Kernel; + +namespace DynamicData.Cache.Internal +{ + internal sealed class GroupOnImmutable + where TKey : notnull + where TGroupKey : notnull + { + private readonly Func _groupSelectorKey; + + private readonly IObservable _regrouper; + + private readonly IObservable> _source; + + public GroupOnImmutable(IObservable> source, Func groupSelectorKey, IObservable? regrouper) + { + _source = source ?? throw new ArgumentNullException(nameof(source)); + _groupSelectorKey = groupSelectorKey ?? throw new ArgumentNullException(nameof(groupSelectorKey)); + _regrouper = regrouper ?? Observable.Never(); + } + + public IObservable> Run() + { + return Observable.Create>( + observer => + { + var locker = new object(); + var grouper = new Grouper(_groupSelectorKey); + + var groups = _source.Synchronize(locker).Select(grouper.Update).Where(changes => changes.Count != 0); + + var regroup = _regrouper.Synchronize(locker).Select(_ => grouper.Regroup()).Where(changes => changes.Count != 0); + + return groups.Merge(regroup).SubscribeSafe(observer); + }); + } + + private sealed class Grouper + { + private readonly IDictionary _allGroupings = new Dictionary(); + + private readonly Func _groupSelectorKey; + + private readonly IDictionary _itemCache = new Dictionary(); + + public Grouper(Func groupSelectorKey) + { + _groupSelectorKey = groupSelectorKey; + } + + public IImmutableGroupChangeSet Regroup() + { + // re-evaluate all items in the group + var items = _itemCache.Select(item => new Change(ChangeReason.Refresh, item.Key, item.Value.Item)); + return HandleUpdates(new ChangeSet(items)); + } + + public IImmutableGroupChangeSet Update(IChangeSet updates) + { + return HandleUpdates(updates); + } + + private static Exception CreateMissingKeyException(ChangeReason reason, TKey key) + { + var message = $"{key} is missing from previous group on {reason}." + $"{Environment.NewLine}Object type {typeof(TObject)}, Key type {typeof(TKey)}, Group key type {typeof(TGroupKey)}"; + return new MissingKeyException(message); + } + + private static IGrouping GetGroupState(GroupCache grouping) + { + return new ImmutableGroup(grouping.Key, grouping.Cache); + } + + private static IGrouping GetGroupState(TGroupKey key, ICache cache) + { + return new ImmutableGroup(key, cache); + } + + private IImmutableGroupChangeSet CreateChangeSet(IDictionary> initialGroupState) + { + var result = new List, TGroupKey>>(); + foreach (var initialGroup in initialGroupState) + { + var key = initialGroup.Key; + var current = _allGroupings[initialGroup.Key]; + + if (current.Cache.Count == 0) + { + _allGroupings.Remove(key); + result.Add(new Change, TGroupKey>(ChangeReason.Remove, key, initialGroup.Value)); + } + else + { + var currentState = GetGroupState(current); + if (initialGroup.Value.Count == 0) + { + result.Add(new Change, TGroupKey>(ChangeReason.Add, key, currentState)); + } + else + { + var previousState = Optional.Some(initialGroup.Value); + result.Add(new Change, TGroupKey>(ChangeReason.Update, key, currentState, previousState)); + } + } + } + + return new ImmutableGroupChangeSet(result); + } + + private Tuple GetCache(TGroupKey key) + { + var cache = _allGroupings.Lookup(key); + if (cache.HasValue) + { + return Tuple.Create(cache.Value, false); + } + + var newcache = new GroupCache(key); + _allGroupings[key] = newcache; + return Tuple.Create(newcache, true); + } + + private IImmutableGroupChangeSet HandleUpdates(IEnumerable> changes) + { + // need to keep track of effected groups to calculate correct notifications + var initialStateOfGroups = new Dictionary>(); + + // 1. Group all items + var grouped = changes.Select(u => new ChangeWithGroup(u, _groupSelectorKey)).GroupBy(c => c.GroupKey); + + // 2. iterate and maintain child caches + grouped.ForEach( + group => + { + var groupItem = GetCache(group.Key); + var groupCache = groupItem.Item1; + var cacheToModify = groupCache.Cache; + + if (!initialStateOfGroups.ContainsKey(group.Key)) + { + initialStateOfGroups[group.Key] = GetGroupState(groupCache); + } + + // 1. Iterate through group changes and maintain the current group + foreach (var current in group) + { + switch (current.Reason) + { + case ChangeReason.Add: + { + cacheToModify.AddOrUpdate(current.Item, current.Key); + _itemCache[current.Key] = current; + break; + } + + case ChangeReason.Update: + { + cacheToModify.AddOrUpdate(current.Item, current.Key); + + // check whether the previous item was in a different group. If so remove from old group + var previous = _itemCache.Lookup(current.Key).ValueOrThrow(() => CreateMissingKeyException(ChangeReason.Update, current.Key)); + + if (!previous.GroupKey.Equals(current.GroupKey)) + { + RemoveFromOldGroup(initialStateOfGroups, previous.GroupKey, current.Key); + } + + _itemCache[current.Key] = current; + break; + } + + case ChangeReason.Remove: + { + var existing = cacheToModify.Lookup(current.Key); + if (existing.HasValue) + { + cacheToModify.Remove(current.Key); + } + else + { + // this has been removed due to an underlying evaluate resulting in a remove + var previousGroupKey = _itemCache.Lookup(current.Key).ValueOrThrow(() => CreateMissingKeyException(ChangeReason.Remove, current.Key)).GroupKey; + + RemoveFromOldGroup(initialStateOfGroups, previousGroupKey, current.Key); + } + + _itemCache.Remove(current.Key); + + break; + } + + case ChangeReason.Refresh: + { + // check whether the previous item was in a different group. If so remove from old group + var previous = _itemCache.Lookup(current.Key); + + previous.IfHasValue( + p => + { + if (p.GroupKey.Equals(current.GroupKey)) + { + return; + } + + RemoveFromOldGroup(initialStateOfGroups, p.GroupKey, current.Key); + + // add to new group because the group value has changed + cacheToModify.AddOrUpdate(current.Item, current.Key); + }).Else( + () => + { + // must be created due to addition + cacheToModify.AddOrUpdate(current.Item, current.Key); + }); + + _itemCache[current.Key] = current; + break; + } + } + } + }); + + // 2. Produce and fire notifications [compare current and previous state] + return CreateChangeSet(initialStateOfGroups); + } + + private void RemoveFromOldGroup(IDictionary> groupState, TGroupKey groupKey, TKey currentKey) + { + _allGroupings.Lookup(groupKey).IfHasValue( + g => + { + if (!groupState.ContainsKey(g.Key)) + { + groupState[g.Key] = GetGroupState(g.Key, g.Cache); + } + + g.Cache.Remove(currentKey); + }); + } + + private readonly struct ChangeWithGroup : IEquatable + { + public ChangeWithGroup(Change change, Func keySelector) + { + GroupKey = keySelector(change.Current); + Item = change.Current; + Key = change.Key; + Reason = change.Reason; + } + + public TObject Item { get; } + + public TKey Key { get; } + + public TGroupKey GroupKey { get; } + + public ChangeReason Reason { get; } + + public static bool operator ==(ChangeWithGroup left, ChangeWithGroup right) + { + return left.Equals(right); + } + + public static bool operator !=(ChangeWithGroup left, ChangeWithGroup right) + { + return !left.Equals(right); + } + + public bool Equals(ChangeWithGroup other) + { + return Key.Equals(other.Key); + } + + public override bool Equals(object? obj) + { + return obj is ChangeWithGroup changeGroup && Equals(changeGroup); + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + + public override string ToString() + { + return $"Key: {Key}, GroupKey: {GroupKey}, Item: {Item}"; + } + } + + private class GroupCache + { + public GroupCache(TGroupKey key) + { + Key = key; + Cache = new Cache(); + } + + public Cache Cache { get; } + + public TGroupKey Key { get; } + } + } + } +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/GroupOnProperty.cs b/src/DynamicData/Cache/Internal/GroupOnProperty.cs index 8407f0ed6..8d2c5e0d2 100644 --- a/src/DynamicData/Cache/Internal/GroupOnProperty.cs +++ b/src/DynamicData/Cache/Internal/GroupOnProperty.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,23 +7,30 @@ using System.Linq.Expressions; using System.Reactive.Concurrency; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { - internal class GroupOnProperty - where TObject: INotifyPropertyChanged + internal class GroupOnProperty + where TObject : INotifyPropertyChanged + where TKey : notnull + where TGroup : notnull { - private readonly IObservable> _source; + private readonly Func _groupSelector; + private readonly Expression> _propertySelector; + + private readonly IScheduler? _scheduler; + + private readonly IObservable> _source; + private readonly TimeSpan? _throttle; - private readonly IScheduler _scheduler; - private readonly Func _groupSelector; - public GroupOnProperty(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler scheduler = null) + public GroupOnProperty(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); - _groupSelector = groupSelectorKey?.Compile() ?? throw new ArgumentNullException(nameof(groupSelectorKey)); + _groupSelector = groupSelectorKey.Compile(); _propertySelector = groupSelectorKey; _throttle = throttle; _scheduler = scheduler; @@ -31,20 +38,21 @@ public GroupOnProperty(IObservable> source, Expression public IObservable> Run() { - return _source.Publish(shared => - { - // Monitor explicit property changes - var regrouper = shared.WhenValueChanged(_propertySelector, false).ToUnit(); - - //add a throttle if specified - if (_throttle != null) - { - regrouper = regrouper.Throttle(_throttle.Value, _scheduler ?? Scheduler.Default); - } - - // Use property changes as a trigger to re-evaluate Grouping - return shared.Group(_groupSelector, regrouper); - }); + return _source.Publish( + shared => + { + // Monitor explicit property changes + var regrouper = shared.WhenValueChanged(_propertySelector, false).ToUnit(); + + // add a throttle if specified + if (_throttle is not null) + { + regrouper = regrouper.Throttle(_throttle.Value, _scheduler ?? Scheduler.Default); + } + + // Use property changes as a trigger to re-evaluate Grouping + return shared.Group(_groupSelector, regrouper); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/GroupOnPropertyWithImmutableState.cs b/src/DynamicData/Cache/Internal/GroupOnPropertyWithImmutableState.cs index 79fe7155b..75b5a96a7 100644 --- a/src/DynamicData/Cache/Internal/GroupOnPropertyWithImmutableState.cs +++ b/src/DynamicData/Cache/Internal/GroupOnPropertyWithImmutableState.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,44 +7,52 @@ using System.Linq.Expressions; using System.Reactive.Concurrency; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class GroupOnPropertyWithImmutableState + where TKey : notnull + where TGroup : notnull where TObject : INotifyPropertyChanged { - private readonly IObservable> _source; + private readonly Func _groupSelector; + private readonly Expression> _propertySelector; - private readonly TimeSpan? _throttle; + private readonly IScheduler _scheduler; - private readonly Func _groupSelector; - public GroupOnPropertyWithImmutableState(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler scheduler = null) + private readonly IObservable> _source; + + private readonly TimeSpan? _throttle; + + public GroupOnPropertyWithImmutableState(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); - _groupSelector = groupSelectorKey?.Compile() ?? throw new ArgumentNullException(nameof(groupSelectorKey)); + _groupSelector = groupSelectorKey.Compile(); _propertySelector = groupSelectorKey; _throttle = throttle; - _scheduler = scheduler; + _scheduler = scheduler ?? Scheduler.Default; } public IObservable> Run() { - return _source.Publish(shared => - { - // Monitor explicit property changes - var regrouper = shared.WhenValueChanged(_propertySelector, false).ToUnit(); - - //add a throttle if specified - if (_throttle != null) - { - regrouper = regrouper.Throttle(_throttle.Value, _scheduler ?? Scheduler.Default); - } - - // Use property changes as a trigger to re-evaluate Grouping - return shared.GroupWithImmutableState(_groupSelector, regrouper); - }); + return _source.Publish( + shared => + { + // Monitor explicit property changes + var regrouper = shared.WhenValueChanged(_propertySelector, false).ToUnit(); + + // add a throttle if specified + if (_throttle is not null) + { + regrouper = regrouper.Throttle(_throttle.Value, _scheduler); + } + + // Use property changes as a trigger to re-evaluate Grouping + return shared.GroupWithImmutableState(_groupSelector, regrouper); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/IFilter.cs b/src/DynamicData/Cache/Internal/IFilter.cs index 9c587e652..138d4a82b 100644 --- a/src/DynamicData/Cache/Internal/IFilter.cs +++ b/src/DynamicData/Cache/Internal/IFilter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,10 +7,31 @@ namespace DynamicData.Cache.Internal { + /// + /// Provides a filter. + /// + /// The type of the object. + /// The type of the field. internal interface IFilter + where TKey : notnull { + /// + /// Gets the filter to use. + /// Func Filter { get; } + + /// + /// Provides a change set with refreshed items. + /// + /// The items to refresh. + /// A change set of the changes. IChangeSet Refresh(IEnumerable> items); + + /// + /// Provides a change set with updated items. + /// + /// The items to update. + /// A change set of the changes. IChangeSet Update(IChangeSet updates); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/IKeySelector.cs b/src/DynamicData/Cache/Internal/IKeySelector.cs index c35154b3f..4e9b17abc 100644 --- a/src/DynamicData/Cache/Internal/IKeySelector.cs +++ b/src/DynamicData/Cache/Internal/IKeySelector.cs @@ -1,11 +1,21 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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.Cache.Internal { - internal interface IKeySelector //: IKeySelector + /// + /// Selects a key from a item. + /// + /// The type of the object. + /// The type of the key. + internal interface IKeySelector // : IKeySelector { + /// + /// Gets the key from the object. + /// + /// The item to get the key for. + /// The key. TKey GetKey(TObject item); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/ImmutableGroup.cs b/src/DynamicData/Cache/Internal/ImmutableGroup.cs index 40958fcda..c73ac02c0 100644 --- a/src/DynamicData/Cache/Internal/ImmutableGroup.cs +++ b/src/DynamicData/Cache/Internal/ImmutableGroup.cs @@ -1,19 +1,20 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class ImmutableGroup : IGrouping, IEquatable> + where TKey : notnull + where TGroupKey : notnull { private readonly ICache _cache; - public TGroupKey Key { get; } - internal ImmutableGroup(TGroupKey key, ICache cache) { Key = key; @@ -22,45 +23,38 @@ internal ImmutableGroup(TGroupKey key, ICache cache) } public int Count => _cache.Count; + public IEnumerable Items => _cache.Items; - public IEnumerable> KeyValues => _cache.KeyValues; + + public TGroupKey Key { get; } + public IEnumerable Keys => _cache.Keys; - public Optional Lookup(TKey key) + public IEnumerable> KeyValues => _cache.KeyValues; + + public static bool operator ==(ImmutableGroup left, ImmutableGroup right) { - return _cache.Lookup(key); + return Equals(left, right); } - #region Equality - - public bool Equals(ImmutableGroup other) + public static bool operator !=(ImmutableGroup left, ImmutableGroup right) { - if (ReferenceEquals(null, other)) - { - return false; - } + return !Equals(left, right); + } + public bool Equals(ImmutableGroup? other) + { if (ReferenceEquals(this, other)) { return true; } - return EqualityComparer.Default.Equals(Key, other.Key); + return other is not null && EqualityComparer.Default.Equals(Key, other.Key); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return obj is ImmutableGroup && Equals((ImmutableGroup) obj); + return obj is ImmutableGroup value && Equals(value); } public override int GetHashCode() @@ -68,18 +62,11 @@ public override int GetHashCode() return EqualityComparer.Default.GetHashCode(Key); } - public static bool operator ==(ImmutableGroup left, ImmutableGroup right) - { - return Equals(left, right); - } - - public static bool operator !=(ImmutableGroup left, ImmutableGroup right) + public Optional Lookup(TKey key) { - return !Equals(left, right); + return _cache.Lookup(key); } - #endregion - public override string ToString() { return $"Grouping for: {Key} ({Count} items)"; diff --git a/src/DynamicData/Cache/Internal/ImmutableGroupChangeSet.cs b/src/DynamicData/Cache/Internal/ImmutableGroupChangeSet.cs index 8df0b0709..d0a61e540 100644 --- a/src/DynamicData/Cache/Internal/ImmutableGroupChangeSet.cs +++ b/src/DynamicData/Cache/Internal/ImmutableGroupChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,16 +7,17 @@ namespace DynamicData.Cache.Internal { internal sealed class ImmutableGroupChangeSet : ChangeSet, TGroupKey>, IImmutableGroupChangeSet + where TKey : notnull + where TGroupKey : notnull { + public static new readonly IImmutableGroupChangeSet Empty = new ImmutableGroupChangeSet(); - public new static readonly IImmutableGroupChangeSet Empty = new ImmutableGroupChangeSet(); - - private ImmutableGroupChangeSet() + public ImmutableGroupChangeSet(IEnumerable, TGroupKey>> items) + : base(items) { } - public ImmutableGroupChangeSet(IEnumerable, TGroupKey>> items) - : base(items) + private ImmutableGroupChangeSet() { } } diff --git a/src/DynamicData/Cache/Internal/IndexAndNode.cs b/src/DynamicData/Cache/Internal/IndexAndNode.cs index 2b32f0edf..0fdcb754b 100644 --- a/src/DynamicData/Cache/Internal/IndexAndNode.cs +++ b/src/DynamicData/Cache/Internal/IndexAndNode.cs @@ -1,11 +1,21 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace DynamicData.Cache.Internal { + internal static class IndexAndNode + { + public static IndexAndNode Create(int index, LinkedListNode value) + { + return new(index, value); + } + } + + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Same class name, different generics.")] internal class IndexAndNode { public IndexAndNode(int index, LinkedListNode node) @@ -15,14 +25,7 @@ public IndexAndNode(int index, LinkedListNode node) } public int Index { get; } - public LinkedListNode Node { get; } - } - internal static class IndexAndNode - { - public static IndexAndNode Create(int index, LinkedListNode value) - { - return new IndexAndNode(index, value); - } + public LinkedListNode Node { get; } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/IndexCalculator.cs b/src/DynamicData/Cache/Internal/IndexCalculator.cs index 0c3c5f773..04391fff6 100644 --- a/src/DynamicData/Cache/Internal/IndexCalculator.cs +++ b/src/DynamicData/Cache/Internal/IndexCalculator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -14,93 +14,33 @@ namespace DynamicData.Cache.Internal /// and apply indexed changes with no need to apply ant expensive IndexOf() operations. /// internal sealed class IndexCalculator + where TKey : notnull { - private KeyValueComparer _comparer; - private List> _list; - private readonly SortOptimisations _optimisations; + private KeyValueComparer _comparer; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The comparer to use. + /// Selected indexing optimisations. public IndexCalculator(KeyValueComparer comparer, SortOptimisations optimisations) { _comparer = comparer; _optimisations = optimisations; - _list = new List>(); + List = new List>(); } - /// - /// Initialises the specified changes. - /// - /// The cache. - /// - public IChangeSet Load(ChangeAwareCache cache) - { - //for the first batch of changes may have arrived before the comparer was set. - //therefore infer the first batch of changes from the cache - _list = cache.KeyValues.OrderBy(kv => kv, _comparer).ToList(); - var initialItems = _list.Select((t, index) => new Change(ChangeReason.Add, t.Key, t.Value, index)); - return new ChangeSet(initialItems); - } - - /// - /// Initialises the specified changes. - /// - /// The cache. - /// - public void Reset(ChangeAwareCache cache) - { - _list = cache.KeyValues.OrderBy(kv => kv, _comparer).ToList(); - } - - public IChangeSet ChangeComparer(KeyValueComparer comparer) - { - _comparer = comparer; - return ChangeSet.Empty; - } - - public IChangeSet Reorder() - { - var result = new List>(); - - if (_optimisations.HasFlag(SortOptimisations.IgnoreEvaluates)) - { - //reorder entire sequence and do not calculate moves - _list = _list.OrderBy(kv => kv, _comparer).ToList(); - } - else - { - int index = -1; - var sorted = _list.OrderBy(t => t, _comparer).ToList(); - foreach (var item in sorted) - { - KeyValuePair current = item; - index++; - - //Cannot use binary search as Resort is implicit of a mutable change - KeyValuePair existing = _list[index]; - var areequal = EqualityComparer.Default.Equals(current.Key, existing.Key); - if (areequal) - { - continue; - } - - var old = _list.IndexOf(current); - _list.RemoveAt(old); - _list.Insert(index, current); - - result.Add(new Change(current.Key, current.Value, index, old)); - } - } + public IComparer> Comparer => _comparer; - return new ChangeSet(result); - } + public List> List { get; private set; } /// - /// Dynamic calculation of moved items which produce a result which can be enumerated through in order + /// Dynamic calculation of moved items which produce a result which can be enumerated through in order. /// - /// + /// The change set. + /// A change set with the calculations. public IChangeSet Calculate(IChangeSet changes) { var result = new List>(changes.Count); @@ -115,7 +55,7 @@ public IChangeSet Calculate(IChangeSet changes) case ChangeReason.Add: { var position = GetInsertPositionBinary(current); - _list.Insert(position, current); + List.Insert(position, current); result.Add(new Change(ChangeReason.Add, u.Key, u.Current, position)); } @@ -126,14 +66,12 @@ public IChangeSet Calculate(IChangeSet changes) { var previous = new KeyValuePair(u.Key, u.Previous.Value); var old = GetCurrentPosition(previous); - _list.RemoveAt(old); + List.RemoveAt(old); - var newposition = GetInsertPositionBinary(current); - _list.Insert(newposition, current); + var newPosition = GetInsertPositionBinary(current); + List.Insert(newPosition, current); - result.Add(new Change(ChangeReason.Update, - u.Key, - u.Current, u.Previous, newposition, old)); + result.Add(new Change(ChangeReason.Update, u.Key, u.Current, u.Previous, newPosition, old)); } break; @@ -141,7 +79,7 @@ public IChangeSet Calculate(IChangeSet changes) case ChangeReason.Remove: { var position = GetCurrentPosition(current); - _list.RemoveAt(position); + List.RemoveAt(position); result.Add(new Change(ChangeReason.Remove, u.Key, u.Current, position)); } @@ -157,52 +95,112 @@ public IChangeSet Calculate(IChangeSet changes) } } - //for evaluates, check whether the change forces a new position - var evaluates = refreshes.OrderByDescending(x => new KeyValuePair(x.Key, x.Current), _comparer) - .ToList(); + // for evaluates, check whether the change forces a new position + var evaluates = refreshes.OrderByDescending(x => new KeyValuePair(x.Key, x.Current), _comparer).ToList(); if (evaluates.Count != 0 && _optimisations.HasFlag(SortOptimisations.IgnoreEvaluates)) { - //reorder entire sequence and do not calculate moves - _list = _list.OrderBy(kv => kv, _comparer).ToList(); + // reorder entire sequence and do not calculate moves + List = List.OrderBy(kv => kv, _comparer).ToList(); } else { - //calculate moves. Very expensive operation - //TODO: Try and make this better + // calculate moves. Very expensive operation + // TODO: Try and make this better foreach (var u in evaluates) { var current = new KeyValuePair(u.Key, u.Current); - var old = _list.IndexOf(current); + var old = List.IndexOf(current); if (old == -1) { continue; } - int newposition = GetInsertPositionLinear(_list, current); + int newPosition = GetInsertPositionLinear(List, current); - if (old < newposition) + if (old < newPosition) { - newposition--; + newPosition--; } - if (old == newposition) + if (old == newPosition) { continue; } - _list.RemoveAt(old); - _list.Insert(newposition, current); - result.Add(new Change(u.Key, u.Current, newposition, old)); + List.RemoveAt(old); + List.Insert(newPosition, current); + result.Add(new Change(u.Key, u.Current, newPosition, old)); } } return new ChangeSet(result); } - public IComparer> Comparer => _comparer; + public IChangeSet ChangeComparer(KeyValueComparer comparer) + { + _comparer = comparer; + return ChangeSet.Empty; + } - public List> List => _list; + /// + /// Initialises the specified changes. + /// + /// The cache. + /// The change set. + public IChangeSet Load(ChangeAwareCache cache) + { + // for the first batch of changes may have arrived before the comparer was set. + // therefore infer the first batch of changes from the cache + List = cache.KeyValues.OrderBy(kv => kv, _comparer).ToList(); + var initialItems = List.Select((t, index) => new Change(ChangeReason.Add, t.Key, t.Value, index)); + return new ChangeSet(initialItems); + } + + public IChangeSet Reorder() + { + var result = new List>(); + + if (_optimisations.HasFlag(SortOptimisations.IgnoreEvaluates)) + { + // reorder entire sequence and do not calculate moves + List = List.OrderBy(kv => kv, _comparer).ToList(); + } + else + { + int index = -1; + foreach (var item in List.OrderBy(t => t, _comparer).ToList()) + { + KeyValuePair current = item; + index++; + + // Cannot use binary search as Resort is implicit of a mutable change + KeyValuePair existing = List[index]; + var areEqual = EqualityComparer.Default.Equals(current.Key, existing.Key); + if (areEqual) + { + continue; + } + + var old = List.IndexOf(current); + List.RemoveAt(old); + List.Insert(index, current); + + result.Add(new Change(current.Key, current.Value, index, old)); + } + } + + return new ChangeSet(result); + } + + /// + /// Initialises the specified changes. + /// + /// The cache. + public void Reset(ChangeAwareCache cache) + { + List = cache.KeyValues.OrderBy(kv => kv, _comparer).ToList(); + } private int GetCurrentPosition(KeyValuePair item) { @@ -210,7 +208,7 @@ private int GetCurrentPosition(KeyValuePair item) if (_optimisations.HasFlag(SortOptimisations.ComparesImmutableValuesOnly)) { - index = _list.BinarySearch(item, _comparer); + index = List.BinarySearch(item, _comparer); if (index < 0) { @@ -219,7 +217,7 @@ private int GetCurrentPosition(KeyValuePair item) } else { - index = _list.IndexOf(item); + index = List.IndexOf(item); if (index < 0) { @@ -230,35 +228,34 @@ private int GetCurrentPosition(KeyValuePair item) return index; } - private int GetInsertPositionLinear(IList> list, KeyValuePair item) + private int GetInsertPositionBinary(KeyValuePair item) { - for (var i = 0; i < list.Count; i++) + int index = List.BinarySearch(item, _comparer); + + if (index > 0) { - if (_comparer.Compare(item, list[i]) < 0) + var tempIndex = index; + index = List.BinarySearch(tempIndex - 1, List.Count - tempIndex, item, _comparer); + if (index > 0) { - return i; + return tempIndex; } } - return _list.Count; + return ~index; } - private int GetInsertPositionBinary(KeyValuePair item) + private int GetInsertPositionLinear(IList> list, KeyValuePair item) { - int index = _list.BinarySearch(item, _comparer); - - if (index > 0) + for (var i = 0; i < list.Count; i++) { - var indx = index; - index = _list.BinarySearch(indx - 1, _list.Count - indx, item, _comparer); - if (index > 0) + if (_comparer.Compare(item, list[i]) < 0) { - return indx; + return i; } } - int insertIndex = ~index; - return insertIndex; + return List.Count; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/InnerJoin.cs b/src/DynamicData/Cache/Internal/InnerJoin.cs index f30e1ecdd..6a30be73e 100644 --- a/src/DynamicData/Cache/Internal/InnerJoin.cs +++ b/src/DynamicData/Cache/Internal/InnerJoin.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,16 +9,18 @@ namespace DynamicData.Cache.Internal { internal class InnerJoin + where TLeftKey : notnull + where TRightKey : notnull { private readonly IObservable> _left; + + private readonly Func _resultSelector; + private readonly IObservable> _right; + private readonly Func _rightKeySelector; - private readonly Func _resultSelector; - public InnerJoin(IObservable> left, - IObservable> right, - Func rightKeySelector, - Func resultSelector) + public InnerJoin(IObservable> left, IObservable> right, Func rightKeySelector, Func resultSelector) { _left = left ?? throw new ArgumentNullException(nameof(left)); _right = right ?? throw new ArgumentNullException(nameof(right)); @@ -28,105 +30,105 @@ public InnerJoin(IObservable> left, public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); + return Observable.Create>( + observer => + { + var locker = new object(); - //create local backing stores - var leftCache = _left.Synchronize(locker).AsObservableCache(); - var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(); + // create local backing stores + var leftCache = _left.Synchronize(locker).AsObservableCache(); + var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(); - //joined is the final cache - var joinedCache = new LockFreeObservableCache(); + // joined is the final cache + var joinedCache = new LockFreeObservableCache(); - var leftLoader = leftCache.Connect() - .Subscribe(changes => - { - joinedCache.Edit(innerCache => - { - foreach (var change in changes.ToConcreteType()) - { - var left = change.Current; - var right = rightCache.Lookup(change.Key); - - switch (change.Reason) + var leftLoader = leftCache.Connect().Subscribe( + changes => { - case ChangeReason.Add: - case ChangeReason.Update: - { - if (right.HasValue) - { - innerCache.AddOrUpdate(_resultSelector(change.Key, left, right.Value), change.Key); - } - else - { - innerCache.Remove(change.Key); - } - - break; - } - - case ChangeReason.Remove: - innerCache.Remove(change.Key); - break; - case ChangeReason.Refresh: - //propagate upstream - innerCache.Refresh(change.Key); - break; - } - } - }); - }); + joinedCache.Edit( + innerCache => + { + foreach (var change in changes.ToConcreteType()) + { + var left = change.Current; + var right = rightCache.Lookup(change.Key); - var rightLoader = rightCache.Connect() - .Subscribe(changes => - { - joinedCache.Edit(innerCache => - { - foreach (var change in changes.ToConcreteType()) - { - var right = change.Current; - var left = leftCache.Lookup(change.Key); - - switch (change.Reason) + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + { + if (right.HasValue) + { + innerCache.AddOrUpdate(_resultSelector(change.Key, left, right.Value), change.Key); + } + else + { + innerCache.Remove(change.Key); + } + + break; + } + + case ChangeReason.Remove: + innerCache.Remove(change.Key); + break; + + case ChangeReason.Refresh: + // propagate upstream + innerCache.Refresh(change.Key); + break; + } + } + }); + }); + + var rightLoader = rightCache.Connect().Subscribe( + changes => { - case ChangeReason.Add: - case ChangeReason.Update: - { - if (left.HasValue) - { - innerCache.AddOrUpdate(_resultSelector(change.Key, left.Value, right), change.Key); - } - else - { - innerCache.Remove(change.Key); - } - } - - break; - case ChangeReason.Remove: - { - innerCache.Remove(change.Key); - } - - break; - case ChangeReason.Refresh: - //propagate upstream - innerCache.Refresh(change.Key); - break; - } - } - }); - }); + joinedCache.Edit( + innerCache => + { + foreach (var change in changes.ToConcreteType()) + { + var right = change.Current; + var left = leftCache.Lookup(change.Key); + + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + { + if (left.HasValue) + { + innerCache.AddOrUpdate(_resultSelector(change.Key, left.Value, right), change.Key); + } + else + { + innerCache.Remove(change.Key); + } + } - return new CompositeDisposable( - joinedCache.Connect().NotEmpty().SubscribeSafe(observer), - leftCache, - rightCache, - leftLoader, - rightLoader, - joinedCache); - }); + break; + + case ChangeReason.Remove: + { + innerCache.Remove(change.Key); + } + + break; + + case ChangeReason.Refresh: + // propagate upstream + innerCache.Refresh(change.Key); + break; + } + } + }); + }); + + return new CompositeDisposable(joinedCache.Connect().NotEmpty().SubscribeSafe(observer), leftCache, rightCache, leftLoader, rightLoader, joinedCache); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/InnerJoinMany.cs b/src/DynamicData/Cache/Internal/InnerJoinMany.cs index 1e9abe091..6b9a27808 100644 --- a/src/DynamicData/Cache/Internal/InnerJoinMany.cs +++ b/src/DynamicData/Cache/Internal/InnerJoinMany.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,16 +7,18 @@ namespace DynamicData.Cache.Internal { internal class InnerJoinMany + where TLeftKey : notnull + where TRightKey : notnull { private readonly IObservable> _left; + + private readonly Func, TDestination> _resultSelector; + private readonly IObservable> _right; + private readonly Func _rightKeySelector; - private readonly Func, TDestination> _resultSelector; - public InnerJoinMany(IObservable> left, - IObservable> right, - Func rightKeySelector, - Func, TDestination> resultSelector) + public InnerJoinMany(IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) { _left = left ?? throw new ArgumentNullException(nameof(left)); _right = right ?? throw new ArgumentNullException(nameof(right)); diff --git a/src/DynamicData/Cache/Internal/KeyComparer.cs b/src/DynamicData/Cache/Internal/KeyComparer.cs index 2338abb24..c1fcf589c 100644 --- a/src/DynamicData/Cache/Internal/KeyComparer.cs +++ b/src/DynamicData/Cache/Internal/KeyComparer.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,12 +10,12 @@ internal sealed class KeyComparer : IEqualityComparer x, KeyValuePair y) { - return x.Key.Equals(y.Key); + return x.Key?.Equals(y.Key) ?? false; } public int GetHashCode(KeyValuePair obj) { - return obj.Key.GetHashCode(); + return obj.Key is null ? 0 : obj.Key.GetHashCode(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/KeySelector.cs b/src/DynamicData/Cache/Internal/KeySelector.cs index 6adcd9848..a4cfef2f3 100644 --- a/src/DynamicData/Cache/Internal/KeySelector.cs +++ b/src/DynamicData/Cache/Internal/KeySelector.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -31,4 +31,4 @@ public TKey GetKey(TObject item) } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/KeySelectorException.cs b/src/DynamicData/Cache/Internal/KeySelectorException.cs index 44e0a2777..34daa5f8c 100644 --- a/src/DynamicData/Cache/Internal/KeySelectorException.cs +++ b/src/DynamicData/Cache/Internal/KeySelectorException.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -13,14 +13,14 @@ namespace DynamicData.Cache.Internal public class KeySelectorException : Exception { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public KeySelectorException() { } /// - /// Initializes a new instance of the class with a specified error message. + /// Initializes a new instance of the class with a specified error message. /// /// The message that describes the error. public KeySelectorException(string message) @@ -29,7 +29,7 @@ public KeySelectorException(string message) } /// - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. /// /// The error message that explains the reason for the exception. The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. public KeySelectorException(string message, Exception innerException) @@ -37,9 +37,14 @@ public KeySelectorException(string message, Exception innerException) { } + /// + /// Initializes a new instance of the class. + /// + /// The serialization info. + /// The serialization context. protected KeySelectorException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) : base(serializationInfo, streamingContext) { } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/KeyValueCollection.cs b/src/DynamicData/Cache/Internal/KeyValueCollection.cs index e482e0399..d6594a1fd 100644 --- a/src/DynamicData/Cache/Internal/KeyValueCollection.cs +++ b/src/DynamicData/Cache/Internal/KeyValueCollection.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -13,10 +13,7 @@ internal class KeyValueCollection : IKeyValueCollection> _items; - public KeyValueCollection(IReadOnlyCollection> items, - IComparer> comparer, - SortReason sortReason, - SortOptimisations optimisations) + public KeyValueCollection(IReadOnlyCollection> items, IComparer> comparer, SortReason sortReason, SortOptimisations optimisations) { _items = items ?? throw new ArgumentNullException(nameof(items)); Comparer = comparer; @@ -32,7 +29,7 @@ public KeyValueCollection() } /// - /// Gets the comparer used to peform the sort + /// Gets the comparer used to perform the sort. /// /// /// The comparer. @@ -41,11 +38,11 @@ public KeyValueCollection() public int Count => _items.Count; - public KeyValuePair this[int index] => _items.ElementAt(index); + public SortOptimisations Optimisations { get; } public SortReason SortReason { get; } - public SortOptimisations Optimisations { get; } + public KeyValuePair this[int index] => _items.ElementAt(index); public IEnumerator> GetEnumerator() { @@ -57,4 +54,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/KeyValueComparer.cs b/src/DynamicData/Cache/Internal/KeyValueComparer.cs index d180d0a8d..d0faec60b 100644 --- a/src/DynamicData/Cache/Internal/KeyValueComparer.cs +++ b/src/DynamicData/Cache/Internal/KeyValueComparer.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,16 +8,16 @@ namespace DynamicData.Cache.Internal { internal class KeyValueComparer : IComparer> { - private readonly IComparer _comparer; + private readonly IComparer? _comparer; - public KeyValueComparer(IComparer comparer = null) + public KeyValueComparer(IComparer? comparer = null) { _comparer = comparer; } public int Compare(KeyValuePair x, KeyValuePair y) { - if (_comparer != null) + if (_comparer is not null) { int result = _comparer.Compare(x.Value, y.Value); @@ -27,7 +27,22 @@ public int Compare(KeyValuePair x, KeyValuePair y) } } + if (x.Key is null && y.Key is null) + { + return 0; + } + + if (x.Key is null) + { + return 1; + } + + if (y.Key is null) + { + return -1; + } + return x.Key.GetHashCode().CompareTo(y.Key.GetHashCode()); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/LeftJoin.cs b/src/DynamicData/Cache/Internal/LeftJoin.cs index 99cb9d7aa..bea94fad1 100644 --- a/src/DynamicData/Cache/Internal/LeftJoin.cs +++ b/src/DynamicData/Cache/Internal/LeftJoin.cs @@ -1,26 +1,28 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class LeftJoin + where TLeftKey : notnull + where TRightKey : notnull { private readonly IObservable> _left; + + private readonly Func, TDestination> _resultSelector; + private readonly IObservable> _right; private readonly Func _rightKeySelector; - private readonly Func, TDestination> _resultSelector; - public LeftJoin(IObservable> left, - IObservable> right, - Func rightKeySelector, - Func, TDestination> resultSelector) + public LeftJoin(IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) { _left = left ?? throw new ArgumentNullException(nameof(left)); _right = right ?? throw new ArgumentNullException(nameof(right)); @@ -30,109 +32,107 @@ public LeftJoin(IObservable> left, public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); + return Observable.Create>( + observer => + { + var locker = new object(); - //create local backing stores - var leftCache = _left.Synchronize(locker).AsObservableCache(false); - var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(false); + // create local backing stores + var leftCache = _left.Synchronize(locker).AsObservableCache(false); + var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(false); - //joined is the final cache - var joinedCache = new LockFreeObservableCache(); + // joined is the final cache + var joinedCache = new LockFreeObservableCache(); - var leftLoader = leftCache.Connect() - .Subscribe(changes => - { - joinedCache.Edit(innerCache => - { - foreach (var change in changes.ToConcreteType()) - { - switch (change.Reason) + var leftLoader = leftCache.Connect().Subscribe( + changes => { - case ChangeReason.Add: - case ChangeReason.Update: - //Update with left (and right if it is presents) - var left = change.Current; - var right = rightCache.Lookup(change.Key); - innerCache.AddOrUpdate(_resultSelector(change.Key, left, right), change.Key); - break; - case ChangeReason.Remove: - //remove from result because a left value is expected - innerCache.Remove(change.Key); - break; - case ChangeReason.Refresh: - //propagate upstream - innerCache.Refresh(change.Key); - break; - } - } - }); - }); + joinedCache.Edit( + innerCache => + { + foreach (var change in changes.ToConcreteType()) + { + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + // Update with left (and right if it is presents) + var left = change.Current; + var right = rightCache.Lookup(change.Key); + innerCache.AddOrUpdate(_resultSelector(change.Key, left, right), change.Key); + break; - var rightLoader = rightCache.Connect() - .Subscribe(changes => - { - joinedCache.Edit(innerCache => - { - foreach (var change in changes.ToConcreteType()) - { - var right = change.Current; - var left = leftCache.Lookup(change.Key); - - switch (change.Reason) + case ChangeReason.Remove: + // remove from result because a left value is expected + innerCache.Remove(change.Key); + break; + + case ChangeReason.Refresh: + // propagate upstream + innerCache.Refresh(change.Key); + break; + } + } + }); + }); + + var rightLoader = rightCache.Connect().Subscribe( + changes => { - case ChangeReason.Add: - case ChangeReason.Update: - { - if (left.HasValue) - { - //Update with left and right value - innerCache.AddOrUpdate(_resultSelector(change.Key, left.Value, right), - change.Key); - } - else - { - //remove if it is already in the cache - innerCache.Remove(change.Key); - } - } - - break; - case ChangeReason.Remove: - { - if (left.HasValue) - { - //Update with no right value - innerCache.AddOrUpdate( - _resultSelector(change.Key, left.Value, Optional.None), - change.Key); - } - else - { - //remove if it is already in the cache - innerCache.Remove(change.Key); - } - } - - break; - case ChangeReason.Refresh: - //propagate upstream - innerCache.Refresh(change.Key); - break; - } - } - }); - }); + joinedCache.Edit( + innerCache => + { + foreach (var change in changes.ToConcreteType()) + { + var right = change.Current; + var left = leftCache.Lookup(change.Key); + + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + { + if (left.HasValue) + { + // Update with left and right value + innerCache.AddOrUpdate(_resultSelector(change.Key, left.Value, right), change.Key); + } + else + { + // remove if it is already in the cache + innerCache.Remove(change.Key); + } + } + + break; - return new CompositeDisposable( - joinedCache.Connect().NotEmpty().SubscribeSafe(observer), - leftCache, - rightCache, - leftLoader, - joinedCache, - rightLoader); - }); + case ChangeReason.Remove: + { + if (left.HasValue) + { + // Update with no right value + innerCache.AddOrUpdate(_resultSelector(change.Key, left.Value, Optional.None), change.Key); + } + else + { + // remove if it is already in the cache + innerCache.Remove(change.Key); + } + } + + break; + + case ChangeReason.Refresh: + // propagate upstream + innerCache.Refresh(change.Key); + break; + } + } + }); + }); + + return new CompositeDisposable(joinedCache.Connect().NotEmpty().SubscribeSafe(observer), leftCache, rightCache, leftLoader, joinedCache, rightLoader); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/LeftJoinMany.cs b/src/DynamicData/Cache/Internal/LeftJoinMany.cs index 555e8ef15..b0c40e5a5 100644 --- a/src/DynamicData/Cache/Internal/LeftJoinMany.cs +++ b/src/DynamicData/Cache/Internal/LeftJoinMany.cs @@ -1,23 +1,26 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class LeftJoinMany + where TLeftKey : notnull + where TRightKey : notnull { private readonly IObservable> _left; + + private readonly Func, TDestination> _resultSelector; + private readonly IObservable> _right; + private readonly Func _rightKeySelector; - private readonly Func, TDestination> _resultSelector; - public LeftJoinMany(IObservable> left, - IObservable> right, - Func rightKeySelector, - Func, TDestination> resultSelector) + public LeftJoinMany(IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) { _left = left ?? throw new ArgumentNullException(nameof(left)); _right = right ?? throw new ArgumentNullException(nameof(right)); @@ -29,12 +32,7 @@ public IObservable> Run() { var emptyCache = Cache.Empty; var rightGrouped = _right.GroupWithImmutableState(_rightKeySelector); - return _left.LeftJoin(rightGrouped, grouping => grouping.Key, - (leftKey, left, grouping) => _resultSelector(leftKey, left, grouping.ValueOr(() => - { - return new ImmutableGroup(leftKey, emptyCache); - }))); + return _left.LeftJoin(rightGrouped, grouping => grouping.Key, (leftKey, left, grouping) => _resultSelector(leftKey, left, grouping.ValueOr(() => new ImmutableGroup(leftKey, emptyCache)))); } - } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/LockFreeObservableCache.cs b/src/DynamicData/Cache/Internal/LockFreeObservableCache.cs index 6566ff1f1..ef7080cf5 100644 --- a/src/DynamicData/Cache/Internal/LockFreeObservableCache.cs +++ b/src/DynamicData/Cache/Internal/LockFreeObservableCache.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,25 +8,32 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { - /// /// An observable cache which exposes an update API. Used at the root - /// of all observable chains + /// of all observable chains. /// /// The type of the object. /// The type of the key. public class LockFreeObservableCache : IObservableCache + where TKey : notnull { - private readonly ChangeAwareCache _innerCache = new ChangeAwareCache(); - private readonly ICacheUpdater _updater; private readonly ISubject> _changes = new Subject>(); + private readonly ISubject> _changesPreview = new Subject>(); - private readonly ISubject _countChanged = new Subject(); + private readonly IDisposable _cleanUp; + + private readonly ISubject _countChanged = new Subject(); + + private readonly ChangeAwareCache _innerCache = new(); + + private readonly ICacheUpdater _updater; + private bool _isDisposed; /// @@ -37,19 +44,21 @@ public LockFreeObservableCache(IObservable> source) { _updater = new CacheUpdater(_innerCache); - var loader = source.Select(changes => - { - _innerCache.Clone(changes); - return _innerCache.CaptureChanges(); - }).SubscribeSafe(_changes); + var loader = source.Select( + changes => + { + _innerCache.Clone(changes); + return _innerCache.CaptureChanges(); + }).SubscribeSafe(_changes); - _cleanUp = Disposable.Create(() => - { - loader.Dispose(); - _changesPreview.OnCompleted(); - _changes.OnCompleted(); - _countChanged.OnCompleted(); - }); + _cleanUp = Disposable.Create( + () => + { + loader.Dispose(); + _changesPreview.OnCompleted(); + _changes.OnCompleted(); + _countChanged.OnCompleted(); + }); } /// @@ -59,62 +68,63 @@ public LockFreeObservableCache() { _updater = new CacheUpdater(_innerCache); - _cleanUp = Disposable.Create(() => - { - _changes.OnCompleted(); - _countChanged.OnCompleted(); - }); + _cleanUp = Disposable.Create( + () => + { + _changes.OnCompleted(); + _countChanged.OnCompleted(); + }); } /// - /// Returns a observable of cache changes preceded with the initial cache state + /// Gets the total count of cached items. /// - /// The result will be filtered using the specified predicate. - /// - public IObservable> Connect(Func predicate = null) - { - return Observable.Defer(() => - { - var initial = _innerCache.GetInitialUpdates(predicate); - var changes = Observable.Return(initial).Concat(_changes); + public int Count => _innerCache.Count; - return (predicate == null ? changes : changes.Filter(predicate)) - .NotEmpty(); - }); - } + /// + /// Gets a count changed observable starting with the current count. + /// + public IObservable CountChanged => _countChanged.StartWith(_innerCache.Count).DistinctUntilChanged(); - /// - public IObservable> Preview(Func predicate = null) - { - return predicate == null ? _changesPreview : _changesPreview.Filter(predicate); - } + /// + /// Gets the Items. + /// + public IEnumerable Items => _innerCache.Items; /// - /// Returns an observable of any changes which match the specified key. The sequence starts with the initial item in the cache (if there is one). + /// Gets the keys. /// - /// The key. - /// - public IObservable> Watch(TKey key) + public IEnumerable Keys => _innerCache.Keys; + + /// + /// Gets the key value pairs. + /// + public IEnumerable> KeyValues => _innerCache.KeyValues; + + /// + /// Returns a observable of cache changes preceded with the initial cache state. + /// + /// The result will be filtered using the specified predicate. + /// An observable that emits the change set. + public IObservable> Connect(Func? predicate = null) { - return Observable.Create> - ( - observer => - { - var initial = _innerCache.Lookup(key); - if (initial.HasValue) + return Observable.Defer( + () => { - observer.OnNext(new Change(ChangeReason.Add, key, initial.Value)); - } + var initial = _innerCache.GetInitialUpdates(predicate); + var changes = Observable.Return(initial).Concat(_changes); - return _changes.Subscribe(changes => - { - var matches = changes.Where(update => update.Key.Equals(key)); - foreach (var match in matches) - { - observer.OnNext(match); - } + return (predicate is null ? changes : changes.Filter(predicate)).NotEmpty(); }); - }); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } /// @@ -123,7 +133,7 @@ public IObservable> Watch(TKey key) /// The edit action. public void Edit(Action> editAction) { - if (editAction == null) + if (editAction is null) { throw new ArgumentNullException(nameof(editAction)); } @@ -132,53 +142,56 @@ public void Edit(Action> editAction) _changes.OnNext(_innerCache.CaptureChanges()); } - /// - /// A count changed observable starting with the current count - /// - public IObservable CountChanged => _countChanged.StartWith(_innerCache.Count).DistinctUntilChanged(); - /// /// Lookup a single item using the specified key. /// /// The key. - /// + /// The looked up value. /// - /// Fast indexed lookup + /// Fast indexed lookup. /// public Optional Lookup(TKey key) { return _innerCache.Lookup(key); } - /// - /// Gets the keys - /// - public IEnumerable Keys => _innerCache.Keys; - - /// - /// Gets the key value pairs - /// - public IEnumerable> KeyValues => _innerCache.KeyValues; + /// + public IObservable> Preview(Func? predicate = null) + { + return predicate is null ? _changesPreview : _changesPreview.Filter(predicate); + } /// - /// Gets the Items + /// Returns an observable of any changes which match the specified key. The sequence starts with the initial item in the cache (if there is one). /// - public IEnumerable Items => _innerCache.Items; + /// The key. + /// An observable that emits the changes. + public IObservable> Watch(TKey key) + { + return Observable.Create>( + observer => + { + var initial = _innerCache.Lookup(key); + if (initial.HasValue) + { + observer.OnNext(new Change(ChangeReason.Add, key, initial.Value)); + } - /// - /// The total count of cached items - /// - public int Count => _innerCache.Count; + return _changes.Subscribe( + changes => + { + foreach (var match in changes.Where(update => update.Key.Equals(key))) + { + observer.OnNext(match); + } + }); + }); + } /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// Disposes of managed and unmanaged resources. /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - + /// If being called by the dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -190,7 +203,7 @@ protected virtual void Dispose(bool isDisposing) if (isDisposing) { - _cleanUp?.Dispose(); + _cleanUp.Dispose(); } } } diff --git a/src/DynamicData/Cache/Internal/ManagedGroup.cs b/src/DynamicData/Cache/Internal/ManagedGroup.cs index df8254743..9dbafd876 100644 --- a/src/DynamicData/Cache/Internal/ManagedGroup.cs +++ b/src/DynamicData/Cache/Internal/ManagedGroup.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,45 +8,34 @@ namespace DynamicData.Cache.Internal { internal sealed class ManagedGroup : IGroup, IDisposable + where TKey : notnull { - private readonly IntermediateCache _cache = new IntermediateCache(); + private readonly IntermediateCache _cache = new(); public ManagedGroup(TGroupKey groupKey) { Key = groupKey; } - internal void Update(Action> updateAction) - { - _cache.Edit(updateAction); - } - - internal int Count => _cache.Count; - - internal IChangeSet GetInitialUpdates() - { - return _cache.GetInitialUpdates(); - } + public IObservableCache Cache => _cache; public TGroupKey Key { get; } - public IObservableCache Cache => _cache; - - #region Equality members + internal int Count => _cache.Count; - private bool Equals(ManagedGroup other) + public void Dispose() { - return EqualityComparer.Default.Equals(Key, other.Key); + _cache.Dispose(); } /// - /// Determines whether the specified is equal to the current . + /// Determines whether the specified is equal to the current . /// /// - /// true if the specified is equal to the current ; otherwise, false. + /// true if the specified is equal to the current ; otherwise, false. /// - /// The to compare with the current . - public override bool Equals(object obj) + /// The to compare with the current . + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -58,36 +47,44 @@ public override bool Equals(object obj) return true; } - return obj is ManagedGroup && Equals((ManagedGroup)obj); + return obj is ManagedGroup managedGroup && Equals(managedGroup); } /// /// Serves as a hash function for a particular type. /// /// - /// A hash code for the current . + /// A hash code for the current . /// public override int GetHashCode() { - return EqualityComparer.Default.GetHashCode(Key); - } - - #endregion - - public void Dispose() - { - _cache.Dispose(); + return Key is null ? 0 : EqualityComparer.Default.GetHashCode(Key); } /// - /// Returns a that represents the current . + /// Returns a that represents the current . /// /// - /// A that represents the current . + /// A that represents the current . /// public override string ToString() { return $"Group: {Key}"; } + + internal IChangeSet GetInitialUpdates() + { + return _cache.GetInitialUpdates(); + } + + internal void Update(Action> updateAction) + { + _cache.Edit(updateAction); + } + + private bool Equals(ManagedGroup other) + { + return EqualityComparer.Default.Equals(Key, other.Key); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/MergeMany.cs b/src/DynamicData/Cache/Internal/MergeMany.cs index 4bfc5685c..143d2bee6 100644 --- a/src/DynamicData/Cache/Internal/MergeMany.cs +++ b/src/DynamicData/Cache/Internal/MergeMany.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,10 +8,12 @@ namespace DynamicData.Cache.Internal { internal class MergeMany + where TKey : notnull { - private readonly IObservable> _source; private readonly Func> _observableSelector; + private readonly IObservable> _source; + public MergeMany(IObservable> source, Func> observableSelector) { _source = source ?? throw new ArgumentNullException(nameof(source)); @@ -20,28 +22,18 @@ public MergeMany(IObservable> source, Func> source, Func> observableSelector) { - if (observableSelector == null) + if (observableSelector is null) { throw new ArgumentNullException(nameof(observableSelector)); } _source = source ?? throw new ArgumentNullException(nameof(source)); - _observableSelector = (t, key) => observableSelector(t); + _observableSelector = (t, _) => observableSelector(t); } public IObservable Run() { - return Observable.Create - ( - observer => _source.SubscribeMany( - (t, key) => _observableSelector(t, key) - .Subscribe( - observer.OnNext, - ex => {}, - ( ) => {} - ) - ) - .Subscribe(t => { }, observer.OnError)); + return Observable.Create(observer => _source.SubscribeMany((t, key) => _observableSelector(t, key).Subscribe(observer.OnNext, _ => { }, () => { })).Subscribe(_ => { }, observer.OnError)); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/MergeManyItems.cs b/src/DynamicData/Cache/Internal/MergeManyItems.cs index e6aef9b54..bcdde837f 100644 --- a/src/DynamicData/Cache/Internal/MergeManyItems.cs +++ b/src/DynamicData/Cache/Internal/MergeManyItems.cs @@ -1,18 +1,21 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class MergeManyItems + where TKey : notnull { - private readonly IObservable> _source; private readonly Func> _observableSelector; + private readonly IObservable> _source; + public MergeManyItems(IObservable> source, Func> observableSelector) { _source = source ?? throw new ArgumentNullException(nameof(source)); @@ -21,24 +24,18 @@ public MergeManyItems(IObservable> source, Func> source, Func> observableSelector) { - if (observableSelector == null) + if (observableSelector is null) { throw new ArgumentNullException(nameof(observableSelector)); } _source = source ?? throw new ArgumentNullException(nameof(source)); - _observableSelector = (t, key) => observableSelector(t); + _observableSelector = (t, _) => observableSelector(t); } public IObservable> Run() { - return Observable.Create> - ( - observer => _source.SubscribeMany((t, v) => _observableSelector(t, v) - .Select(z => new ItemWithValue(t, z)) - .SubscribeSafe(observer)) - .Subscribe() - ); + return Observable.Create>(observer => _source.SubscribeMany((t, v) => _observableSelector(t, v).Select(z => new ItemWithValue(t, z)).SubscribeSafe(observer)).Subscribe()); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/ObservableWithValue.cs b/src/DynamicData/Cache/Internal/ObservableWithValue.cs index 9ab541331..c5e9ad6fa 100644 --- a/src/DynamicData/Cache/Internal/ObservableWithValue.cs +++ b/src/DynamicData/Cache/Internal/ObservableWithValue.cs @@ -1,27 +1,26 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class ObservableWithValue { - private Optional _latestValue = Optional.None; - public ObservableWithValue(TObject item, IObservable source) { Item = item; - Observable = source.Do(value => _latestValue = value); + Observable = source.Do(value => LatestValue = value); } public TObject Item { get; } - public Optional LatestValue => _latestValue; + public Optional LatestValue { get; private set; } = Optional.None; public IObservable Observable { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/Page.cs b/src/DynamicData/Cache/Internal/Page.cs index a663b293f..8c94eb224 100644 --- a/src/DynamicData/Cache/Internal/Page.cs +++ b/src/DynamicData/Cache/Internal/Page.cs @@ -1,20 +1,21 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; + namespace DynamicData.Cache.Internal { internal class Page + where TKey : notnull { - private readonly IObservable> _source; private readonly IObservable _pageRequests; - public Page([NotNull] IObservable> source, - [NotNull] IObservable pageRequests) + private readonly IObservable> _source; + + public Page(IObservable> source, IObservable pageRequests) { _source = source; _pageRequests = pageRequests; @@ -22,33 +23,40 @@ public Page([NotNull] IObservable> source, public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); - var paginator = new Paginator(); - var request = _pageRequests.Synchronize(locker).Select(paginator.Paginate); - var datachange = _source.Synchronize(locker).Select(paginator.Update); - - return request.Merge(datachange).Where(updates => updates != null).SubscribeSafe(observer); - }); + return Observable.Create>( + observer => + { + var locker = new object(); + var paginator = new Paginator(); + var request = _pageRequests.Synchronize(locker).Select(paginator.Paginate); + var dataChange = _source.Synchronize(locker).Select(paginator.Update); + + return request.Merge(dataChange) + .Where(updates => updates is not null) + .Select(x => x!) + .SubscribeSafe(observer); + }); } private sealed class Paginator { private IKeyValueCollection _all = new KeyValueCollection(); + private IKeyValueCollection _current = new KeyValueCollection(); - private IPageRequest _request; + private bool _isLoaded; + private IPageRequest _request; + public Paginator() { _request = PageRequest.Default; _isLoaded = false; } - public IPagedChangeSet Paginate(IPageRequest parameters) + public IPagedChangeSet? Paginate(IPageRequest? parameters) { - if (parameters == null || parameters.Page < 0 || parameters.Size < 1) + if (parameters is null || parameters.Page < 0 || parameters.Size < 1) { return null; } @@ -63,21 +71,34 @@ public IPagedChangeSet Paginate(IPageRequest parameters) return Paginate(); } - public IPagedChangeSet Update(ISortedChangeSet updates) + public IPagedChangeSet? Update(ISortedChangeSet updates) { _isLoaded = true; _all = updates.SortedItems; return Paginate(updates); } - private IPagedChangeSet Paginate(ISortedChangeSet updates = null) + private int CalculatePages() { - if (_isLoaded == false) + if (_request.Size >= _all.Count) { - return null; + return 1; } - if (_request == null) + int pages = _all.Count / _request.Size; + int overlap = _all.Count % _request.Size; + + if (overlap == 0) + { + return pages; + } + + return pages + 1; + } + + private IPagedChangeSet? Paginate(ISortedChangeSet? updates = null) + { + if (_isLoaded == false) { return null; } @@ -88,13 +109,11 @@ private IPagedChangeSet Paginate(ISortedChangeSet int page = _request.Page > pages ? pages : _request.Page; int skip = _request.Size * (page - 1); - var paged = _all.Skip(skip) - .Take(_request.Size) - .ToList(); + var paged = _all.Skip(skip).Take(_request.Size).ToList(); _current = new KeyValueCollection(paged, _all.Comparer, updates?.SortedItems.SortReason ?? SortReason.DataChanged, _all.Optimisations); - //check for changes within the current virtualised page. Notify if there have been changes or if the overall count has changed + // check for changes within the current virtualised page. Notify if there have been changes or if the overall count has changed var notifications = FilteredIndexCalculator.Calculate(_current, previous, updates); if (notifications.Count == 0 && (previous.Count != _current.Count)) { @@ -105,24 +124,6 @@ private IPagedChangeSet Paginate(ISortedChangeSet return new PagedChangeSet(_current, notifications, response); } - - private int CalculatePages() - { - if (_request.Size >= _all.Count) - { - return 1; - } - - int pages = _all.Count / _request.Size; - int overlap = _all.Count % _request.Size; - - if (overlap == 0) - { - return pages; - } - - return pages + 1; - } } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/QueryWhenChanged.cs b/src/DynamicData/Cache/Internal/QueryWhenChanged.cs index 9d69a35e7..f13c7f2fc 100644 --- a/src/DynamicData/Cache/Internal/QueryWhenChanged.cs +++ b/src/DynamicData/Cache/Internal/QueryWhenChanged.cs @@ -1,19 +1,20 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.Cache.Internal { internal class QueryWhenChanged + where TKey : notnull { + private readonly Func>? _itemChangedTrigger; + private readonly IObservable> _source; - private readonly Func> _itemChangedTrigger; - public QueryWhenChanged([NotNull] IObservable> source, Func> itemChangedTrigger = null) + public QueryWhenChanged(IObservable> source, Func>? itemChangedTrigger = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); _itemChangedTrigger = itemChangedTrigger; @@ -21,41 +22,40 @@ public QueryWhenChanged([NotNull] IObservable> source, public IObservable> Run() { - if (_itemChangedTrigger == null) + if (_itemChangedTrigger is null) { - return _source - .Scan((Cache)null, (cache, changes) => - { - if (cache == null) + return _source.Scan( + (Cache?)null, + (cache, changes) => { - cache = new Cache(changes.Count); - } - - cache.Clone(changes); - return cache; - }).Select(list => new AnonymousQuery(list)); + cache ??= new Cache(changes.Count); + + cache.Clone(changes); + return cache; + }) + .Where(x => x is not null) + .Select(x => x!) + .Select(list => new AnonymousQuery(list)); } - return _source.Publish(shared => - { - var locker = new object(); - var state = new Cache(); + return _source.Publish( + shared => + { + var locker = new object(); + var state = new Cache(); - var inlineChange = shared - .MergeMany(_itemChangedTrigger) - .Synchronize(locker) - .Select(_ => new AnonymousQuery(state)); + var inlineChange = shared.MergeMany(_itemChangedTrigger).Synchronize(locker).Select(_ => new AnonymousQuery(state)); - var sourceChanged = shared - .Synchronize(locker) - .Scan(state, (list, changes) => - { - list.Clone(changes); - return list; - }).Select(list => new AnonymousQuery(list)); + var sourceChanged = shared.Synchronize(locker).Scan( + state, + (list, changes) => + { + list.Clone(changes); + return list; + }).Select(list => new AnonymousQuery(list)); - return sourceChanged.Merge(inlineChange); - }); + return sourceChanged.Merge(inlineChange); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/ReaderWriter.cs b/src/DynamicData/Cache/Internal/ReaderWriter.cs index f7b72a782..8f981a346 100644 --- a/src/DynamicData/Cache/Internal/ReaderWriter.cs +++ b/src/DynamicData/Cache/Internal/ReaderWriter.cs @@ -1,114 +1,84 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class ReaderWriter + where TKey : notnull { - private readonly Func _keySelector; - private Dictionary _data = new Dictionary(); //could do with priming this on first time load - private CacheUpdater _activeUpdater; + private readonly Func? _keySelector; - private readonly object _locker = new object(); + private readonly object _locker = new(); - public ReaderWriter(Func keySelector = null) => _keySelector = keySelector; + private CacheUpdater? _activeUpdater; - #region Writers + private Dictionary _data = new(); // could do with priming this on first time load - public ChangeSet Write(IChangeSet changes, Action> previewHandler, bool collectChanges) - { - if (changes == null) - { - throw new ArgumentNullException(nameof(changes)); - } + public ReaderWriter(Func? keySelector = null) => _keySelector = keySelector; - return DoUpdate(updater => updater.Clone(changes), previewHandler, collectChanges); - } - - public ChangeSet Write(Action> updateAction, Action> previewHandler, bool collectChanges) + public int Count { - if (updateAction == null) + get { - throw new ArgumentNullException(nameof(updateAction)); + lock (_locker) + { + return _data.Count; + } } - - return DoUpdate(updateAction, previewHandler, collectChanges); } - public ChangeSet Write(Action> updateAction, Action> previewHandler, bool collectChanges) + public TObject[] Items { - if (updateAction == null) + get { - throw new ArgumentNullException(nameof(updateAction)); + lock (_locker) + { + TObject[] result = new TObject[_data.Count]; + _data.Values.CopyTo(result, 0); + return result; + } } - - return DoUpdate(updateAction, previewHandler, collectChanges); } - private ChangeSet DoUpdate(Action> updateAction, Action> previewHandler, bool collectChanges) + public TKey[] Keys { - lock (_locker) + get { - if (previewHandler != null) - { - var copy = new Dictionary(_data); - var changeAwareCache = new ChangeAwareCache(_data); - - _activeUpdater = new CacheUpdater(changeAwareCache, _keySelector); - updateAction(_activeUpdater); - _activeUpdater = null; - - var changes = changeAwareCache.CaptureChanges(); - - InternalEx.Swap(ref copy, ref _data); - previewHandler(changes); - InternalEx.Swap(ref copy, ref _data); - - return changes; - } - - if (collectChanges) + lock (_locker) { - var changeAwareCache = new ChangeAwareCache(_data); - - _activeUpdater = new CacheUpdater(changeAwareCache, _keySelector); - updateAction(_activeUpdater); - _activeUpdater = null; - - return changeAwareCache.CaptureChanges(); + TKey[] result = new TKey[_data.Count]; + _data.Keys.CopyTo(result, 0); + return result; } - - _activeUpdater = new CacheUpdater(_data, _keySelector); - updateAction(_activeUpdater); - _activeUpdater = null; - - return ChangeSet.Empty; } } - internal void WriteNested(Action> updateAction) + public KeyValuePair[] KeyValues { - lock (_locker) + get { - if (_activeUpdater == null) + lock (_locker) { - throw new InvalidOperationException("WriteNested can only be used if another write is already in progress."); - } + KeyValuePair[] result = new KeyValuePair[_data.Count]; + int i = 0; + foreach (var kvp in _data) + { + result[i] = kvp; + i++; + } - updateAction(_activeUpdater); + return result; + } } } - #endregion - - #region Accessors - - public ChangeSet GetInitialUpdates( Func filter = null) + public ChangeSet GetInitialUpdates(Func? filter = null) { lock (_locker) { @@ -119,13 +89,11 @@ public ChangeSet GetInitialUpdates( Func filter = return ChangeSet.Empty; } - var changes = filter == null - ? new ChangeSet(dictionary.Count) - : new ChangeSet(); + var changes = filter is null ? new ChangeSet(dictionary.Count) : new ChangeSet(); foreach (var kvp in dictionary) { - if (filter == null || filter(kvp.Value)) + if (filter is null || filter(kvp.Value)) { changes.Add(new Change(ChangeReason.Add, kvp.Key, kvp.Value)); } @@ -135,70 +103,96 @@ public ChangeSet GetInitialUpdates( Func filter = } } - public TKey[] Keys + public Optional Lookup(TKey key) { - get + lock (_locker) { - lock (_locker) - { - TKey[] result = new TKey[_data.Count]; - _data.Keys.CopyTo(result, 0); - return result; - } + return _data.Lookup(key); } } - public KeyValuePair[] KeyValues + public ChangeSet Write(IChangeSet changes, Action>? previewHandler, bool collectChanges) { - get + if (changes is null) { - lock (_locker) - { - KeyValuePair[] result = new KeyValuePair[_data.Count]; - int i = 0; - foreach (var kvp in _data) - { - result[i] = kvp; - i++; - } + throw new ArgumentNullException(nameof(changes)); + } - return result; - } + return DoUpdate(updater => updater.Clone(changes), previewHandler, collectChanges); + } + + public ChangeSet Write(Action> updateAction, Action>? previewHandler, bool collectChanges) + { + if (updateAction is null) + { + throw new ArgumentNullException(nameof(updateAction)); } + + return DoUpdate(updateAction, previewHandler, collectChanges); } - public TObject[] Items + public ChangeSet Write(Action> updateAction, Action>? previewHandler, bool collectChanges) { - get + if (updateAction is null) { - lock (_locker) - { - TObject[] result = new TObject[_data.Count]; - _data.Values.CopyTo(result, 0); - return result; - } + throw new ArgumentNullException(nameof(updateAction)); } + + return DoUpdate(updateAction, previewHandler, collectChanges); } - public Optional Lookup(TKey key) + public void WriteNested(Action> updateAction) { lock (_locker) { - return _data.Lookup(key); + if (_activeUpdater is null) + { + throw new InvalidOperationException("WriteNested can only be used if another write is already in progress."); + } + + updateAction(_activeUpdater); } } - public int Count + private ChangeSet DoUpdate(Action> updateAction, Action>? previewHandler, bool collectChanges) { - get + lock (_locker) { - lock (_locker) + if (previewHandler is not null) { - return _data.Count; + var copy = new Dictionary(_data); + var changeAwareCache = new ChangeAwareCache(_data); + + _activeUpdater = new CacheUpdater(changeAwareCache, _keySelector); + updateAction(_activeUpdater); + _activeUpdater = null; + + var changes = changeAwareCache.CaptureChanges(); + + InternalEx.Swap(ref copy, ref _data); + previewHandler(changes); + InternalEx.Swap(ref copy, ref _data); + + return changes; } + + if (collectChanges) + { + var changeAwareCache = new ChangeAwareCache(_data); + + _activeUpdater = new CacheUpdater(changeAwareCache, _keySelector); + updateAction(_activeUpdater); + _activeUpdater = null; + + return changeAwareCache.CaptureChanges(); + } + + _activeUpdater = new CacheUpdater(_data, _keySelector); + updateAction(_activeUpdater); + _activeUpdater = null; + + return ChangeSet.Empty; } } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/RefCount.cs b/src/DynamicData/Cache/Internal/RefCount.cs index 0a5dab364..ae958ca60 100644 --- a/src/DynamicData/Cache/Internal/RefCount.cs +++ b/src/DynamicData/Cache/Internal/RefCount.cs @@ -1,56 +1,65 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.Cache.Internal { internal class RefCount + where TKey : notnull { + private readonly object _locker = new(); + private readonly IObservable> _source; - private readonly object _locker = new object(); + + private IObservableCache? _cache; + private int _refCount; - private IObservableCache _cache; - public RefCount([NotNull] IObservable> source) + public RefCount(IObservable> source) { _source = source ?? throw new ArgumentNullException(nameof(source)); } public IObservable> Run() { - return Observable.Create>(observer => - { - lock (_locker) - { - if (++_refCount == 1) + return Observable.Create>( + observer => { - _cache = _source.AsObservableCache(); - } - } - - var subscriber = _cache.Connect().SubscribeSafe(observer); + lock (_locker) + { + if (++_refCount == 1) + { + _cache = _source.AsObservableCache(); + } + } - return Disposable.Create(() => - { - subscriber.Dispose(); - IDisposable cacheToDispose = null; - lock (_locker) - { - if (--_refCount == 0) + if (_cache is null) { - cacheToDispose = _cache; - _cache = null; + throw new InvalidOperationException(nameof(_cache) + " is null"); } - } - cacheToDispose?.Dispose(); - }); - }); + var subscriber = _cache.Connect().SubscribeSafe(observer); + + return Disposable.Create(() => + { + subscriber.Dispose(); + IDisposable? cacheToDispose = null; + lock (_locker) + { + if (--_refCount == 0) + { + cacheToDispose = _cache; + _cache = null; + } + } + + cacheToDispose?.Dispose(); + }); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/RemoveKeyEnumerator.cs b/src/DynamicData/Cache/Internal/RemoveKeyEnumerator.cs index 17ea4e090..2ebdb9e58 100644 --- a/src/DynamicData/Cache/Internal/RemoveKeyEnumerator.cs +++ b/src/DynamicData/Cache/Internal/RemoveKeyEnumerator.cs @@ -1,26 +1,27 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections; using System.Collections.Generic; -using DynamicData.Annotations; namespace DynamicData.Cache.Internal { internal class RemoveKeyEnumerator : IEnumerable> + where TKey : notnull { + private readonly IExtendedList? _list; + private readonly IChangeSet _source; - private readonly IExtendedList _list; - /// Converts a to - /// The changeset with a key + /// Initializes a new instance of the class.Converts a to . + /// The change set with a key. /// /// An optional list, if provided it allows the refresh from a key based cache to find the index for the resulting list based refresh. /// If not provided a refresh will dropdown to a replace which may ultimately result in a remove+add change downstream. /// - public RemoveKeyEnumerator([NotNull] IChangeSet source, [CanBeNull] IExtendedList list = null) + public RemoveKeyEnumerator(IChangeSet source, IExtendedList? list = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); _list = list; @@ -30,9 +31,8 @@ public RemoveKeyEnumerator([NotNull] IChangeSet source, [CanBeNul /// Returns an enumerator that iterates through the collection. /// /// - /// A that can be used to iterate through the collection. + /// A that can be used to iterate through the collection. /// - /// public IEnumerator> GetEnumerator() { foreach (var change in _source) @@ -42,34 +42,40 @@ public IEnumerator> GetEnumerator() case ChangeReason.Add: yield return new Change(ListChangeReason.Add, change.Current, change.CurrentIndex); break; + case ChangeReason.Refresh: // Refresh needs an index, which we don't have in a Change model since it's key based. // See: DynamicData > Binding > ObservableCollectionAdaptor.cs Line 129-130 // Note: A refresh is not index based within the context of a sorted change. - // Thus, currentIndex will not be available here where as other changes like add and remove do have indexes if coming from a sorted changeset. + // Thus, currentIndex will not be available here where as other changes like add and remove do have indexes if coming from a sorted change set. // In order to properly handle a refresh and map to an index on a list, we need to use the source list (within the edit method so that it's thread safe) - if (_list?.IndexOf(change.Current) is int index && index >= 0) + var index = _list?.IndexOf(change.Current); + if (index >= 0) { - yield return new Change(ListChangeReason.Refresh, current: change.Current, index: index); + yield return new Change(ListChangeReason.Refresh, change.Current, index.Value); } - // Fallback to a replace if a list is not available else { - yield return new Change(ListChangeReason.Replace, current: change.Current, previous: change.Current); + // Fallback to a replace if a list is not available + yield return new Change(ListChangeReason.Replace, change.Current, change.Current); } + break; + case ChangeReason.Moved: // Move is always sorted yield return new Change(change.Current, change.CurrentIndex, change.PreviousIndex); break; + case ChangeReason.Update: - yield return new Change(ListChangeReason.Remove, change.Previous.Value, index: change.PreviousIndex); - yield return new Change(ListChangeReason.Add, change.Current, index: change.CurrentIndex); + yield return new Change(ListChangeReason.Remove, change.Previous.Value, change.PreviousIndex); + yield return new Change(ListChangeReason.Add, change.Current, change.CurrentIndex); break; + case ChangeReason.Remove: - yield return new Change(ListChangeReason.Remove, change.Current, index: change.CurrentIndex); + yield return new Change(ListChangeReason.Remove, change.Current, change.CurrentIndex); break; } } @@ -80,4 +86,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/RightJoin.cs b/src/DynamicData/Cache/Internal/RightJoin.cs index 08d72f590..87fd1140a 100644 --- a/src/DynamicData/Cache/Internal/RightJoin.cs +++ b/src/DynamicData/Cache/Internal/RightJoin.cs @@ -1,25 +1,28 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class RightJoin + where TLeftKey : notnull + where TRightKey : notnull { private readonly IObservable> _left; + + private readonly Func, TRight, TDestination> _resultSelector; + private readonly IObservable> _right; + private readonly Func _rightKeySelector; - private readonly Func, TRight, TDestination> _resultSelector; - public RightJoin(IObservable> left, - IObservable> right, - Func rightKeySelector, - Func, TRight, TDestination> resultSelector) + public RightJoin(IObservable> left, IObservable> right, Func rightKeySelector, Func, TRight, TDestination> resultSelector) { _left = left ?? throw new ArgumentNullException(nameof(left)); _right = right ?? throw new ArgumentNullException(nameof(right)); @@ -29,106 +32,107 @@ public RightJoin(IObservable> left, public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); + return Observable.Create>( + observer => + { + var locker = new object(); - //create local backing stores - var leftCache = _left.Synchronize(locker).AsObservableCache(false); - var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(false); + // create local backing stores + var leftCache = _left.Synchronize(locker).AsObservableCache(false); + var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(false); - //joined is the final cache - var joinedCache = new LockFreeObservableCache(); + // joined is the final cache + var joinedCache = new LockFreeObservableCache(); - var rightLoader = rightCache.Connect() - .Subscribe(changes => - { - joinedCache.Edit(innerCache => - { - foreach (var change in changes.ToConcreteType()) - { - switch (change.Reason) + var rightLoader = rightCache.Connect().Subscribe( + changes => { - case ChangeReason.Add: - case ChangeReason.Update: - //Update with right (and right if it is presents) - var right = change.Current; - var left = leftCache.Lookup(change.Key); - innerCache.AddOrUpdate(_resultSelector(change.Key, left, right), change.Key); - break; - case ChangeReason.Remove: - //remove from result because a right value is expected - innerCache.Remove(change.Key); - break; - case ChangeReason.Refresh: - //propagate upstream - innerCache.Refresh(change.Key); - break; - } - } - }); - }); + joinedCache.Edit( + innerCache => + { + foreach (var change in changes.ToConcreteType()) + { + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + // Update with right (and right if it is presents) + var right = change.Current; + var left = leftCache.Lookup(change.Key); + innerCache.AddOrUpdate(_resultSelector(change.Key, left, right), change.Key); + break; - var leftLoader = leftCache.Connect() - .Subscribe(changes => - { - joinedCache.Edit(innerCache => - { - foreach (var change in changes.ToConcreteType()) - { - TLeft left = change.Current; - Optional right = rightCache.Lookup(change.Key); - - switch (change.Reason) + case ChangeReason.Remove: + // remove from result because a right value is expected + innerCache.Remove(change.Key); + break; + + case ChangeReason.Refresh: + // propagate upstream + innerCache.Refresh(change.Key); + break; + } + } + }); + }); + + var leftLoader = leftCache.Connect().Subscribe( + changes => { - case ChangeReason.Add: - case ChangeReason.Update: - { - if (right.HasValue) - { - //Update with left and right value - innerCache.AddOrUpdate(_resultSelector(change.Key, left, right.Value), change.Key); - } - else - { - //There is no right so remove if already in the cache - innerCache.Remove(change.Key); - } - } - - break; - case ChangeReason.Remove: - { - if (right.HasValue) - { - //Update with no left value - innerCache.AddOrUpdate(_resultSelector(change.Key, Optional.None, right.Value), change.Key); - } - else - { - //remove if it is already in the cache - innerCache.Remove(change.Key); - } - } - - break; - case ChangeReason.Refresh: - //propagate upstream - innerCache.Refresh(change.Key); - break; - } - } - }); - }); + joinedCache.Edit( + innerCache => + { + foreach (var change in changes.ToConcreteType()) + { + TLeft left = change.Current; + Optional right = rightCache.Lookup(change.Key); + + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + { + if (right.HasValue) + { + // Update with left and right value + innerCache.AddOrUpdate(_resultSelector(change.Key, left, right.Value), change.Key); + } + else + { + // There is no right so remove if already in the cache + innerCache.Remove(change.Key); + } + } + + break; - return new CompositeDisposable( - joinedCache.Connect().NotEmpty().SubscribeSafe(observer), - leftCache, - rightCache, - rightLoader, - joinedCache, - leftLoader); - }); + case ChangeReason.Remove: + { + if (right.HasValue) + { + // Update with no left value + innerCache.AddOrUpdate(_resultSelector(change.Key, Optional.None, right.Value), change.Key); + } + else + { + // remove if it is already in the cache + innerCache.Remove(change.Key); + } + } + + break; + + case ChangeReason.Refresh: + // propagate upstream + innerCache.Refresh(change.Key); + break; + } + } + }); + }); + + return new CompositeDisposable(joinedCache.Connect().NotEmpty().SubscribeSafe(observer), leftCache, rightCache, rightLoader, joinedCache, leftLoader); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/RightJoinMany.cs b/src/DynamicData/Cache/Internal/RightJoinMany.cs index df821978f..33ab3f3f7 100644 --- a/src/DynamicData/Cache/Internal/RightJoinMany.cs +++ b/src/DynamicData/Cache/Internal/RightJoinMany.cs @@ -1,24 +1,26 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class RightJoinMany + where TLeftKey : notnull + where TRightKey : notnull { private readonly IObservable> _left; + + private readonly Func, IGrouping, TDestination> _resultSelector; + private readonly IObservable> _right; + private readonly Func _rightKeySelector; - private readonly Func, IGrouping, TDestination> _resultSelector; - public RightJoinMany([NotNull] IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, IGrouping, TDestination> resultSelector) + public RightJoinMany(IObservable> left, IObservable> right, Func rightKeySelector, Func, IGrouping, TDestination> resultSelector) { _left = left ?? throw new ArgumentNullException(nameof(left)); _right = right ?? throw new ArgumentNullException(nameof(right)); diff --git a/src/DynamicData/Cache/Internal/SizeExpirer.cs b/src/DynamicData/Cache/Internal/SizeExpirer.cs index 63b8fef29..073413bbc 100644 --- a/src/DynamicData/Cache/Internal/SizeExpirer.cs +++ b/src/DynamicData/Cache/Internal/SizeExpirer.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,15 +6,18 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class SizeExpirer + where TKey : notnull { - private readonly IObservable> _source; private readonly int _size; + private readonly IObservable> _source; + public SizeExpirer(IObservable> source, int size) { if (size <= 0) @@ -28,30 +31,29 @@ public SizeExpirer(IObservable> source, int size) public IObservable> Run() { - return Observable.Create>(observer => - { - var sizeLimiter = new SizeLimiter(_size); - var root = new IntermediateCache(_source); - - var subscriber = root.Connect() - .Transform((t, v) => new ExpirableItem(t, v, DateTime.Now)) - .Select(changes => + return Observable.Create>( + observer => { - var result = sizeLimiter.Change(changes); - - var removes = result.Where(c => c.Reason == ChangeReason.Remove); - root.Edit(updater => removes.ForEach(c => updater.Remove(c.Key))); - return result; - }) - .Finally(observer.OnCompleted) - .SubscribeSafe(observer); - - return Disposable.Create(() => - { - subscriber.Dispose(); - root.Dispose(); - }); - }); + var sizeLimiter = new SizeLimiter(_size); + var root = new IntermediateCache(_source); + + var subscriber = root.Connect().Transform((t, v) => new ExpirableItem(t, v, DateTime.Now)).Select( + changes => + { + var result = sizeLimiter.Change(changes); + + var removes = result.Where(c => c.Reason == ChangeReason.Remove); + root.Edit(updater => removes.ForEach(c => updater.Remove(c.Key))); + return result; + }).Finally(observer.OnCompleted).SubscribeSafe(observer); + + return Disposable.Create( + () => + { + subscriber.Dispose(); + root.Dispose(); + }); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/SizeLimiter.cs b/src/DynamicData/Cache/Internal/SizeLimiter.cs index 49ef418b4..828938cdf 100644 --- a/src/DynamicData/Cache/Internal/SizeLimiter.cs +++ b/src/DynamicData/Cache/Internal/SizeLimiter.cs @@ -1,16 +1,18 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; using System.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class SizeLimiter + where TKey : notnull { - private readonly ChangeAwareCache, TKey> _cache = new ChangeAwareCache, TKey>(); + private readonly ChangeAwareCache, TKey> _cache = new(); private readonly int _sizeLimit; @@ -23,25 +25,15 @@ public IChangeSet Change(IChangeSet, { _cache.Clone(updates); - var itemstoexpire = _cache.KeyValues - .OrderByDescending(exp => exp.Value.ExpireAt) - .Skip(_sizeLimit) - .Select(exp => new Change(ChangeReason.Remove, exp.Key, exp.Value.Value)) - .ToList(); + var itemsToExpire = _cache.KeyValues.OrderByDescending(exp => exp.Value.ExpireAt).Skip(_sizeLimit).Select(exp => new Change(ChangeReason.Remove, exp.Key, exp.Value.Value)).ToList(); - if (itemstoexpire.Count > 0) + if (itemsToExpire.Count > 0) { - _cache.Remove(itemstoexpire.Select(exp => exp.Key)); + _cache.Remove(itemsToExpire.Select(exp => exp.Key)); } var notifications = _cache.CaptureChanges(); - var changed = notifications.Select(update => new Change - ( - update.Reason, - update.Key, - update.Current.Value, - update.Previous.HasValue ? update.Previous.Value.Value : Optional.None - )); + var changed = notifications.Select(update => new Change(update.Reason, update.Key, update.Current.Value, update.Previous.HasValue ? update.Previous.Value.Value : Optional.None)); return new ChangeSet(changed); } @@ -49,12 +41,9 @@ public IChangeSet Change(IChangeSet, public KeyValuePair[] CloneAndReturnExpiredOnly(IChangeSet, TKey> updates) { _cache.Clone(updates); - _cache.CaptureChanges(); //Clear any changes + _cache.CaptureChanges(); // Clear any changes - return _cache.KeyValues.OrderByDescending(exp => exp.Value.Index) - .Skip(_sizeLimit) - .Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Value)) - .ToArray(); + return _cache.KeyValues.OrderByDescending(exp => exp.Value.Index).Skip(_sizeLimit).Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Value)).ToArray(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/Sort.cs b/src/DynamicData/Cache/Internal/Sort.cs index f44ef72fc..91652ce9c 100644 --- a/src/DynamicData/Cache/Internal/Sort.cs +++ b/src/DynamicData/Cache/Internal/Sort.cs @@ -1,10 +1,9 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reactive; using System.Reactive.Linq; @@ -12,23 +11,23 @@ namespace DynamicData.Cache.Internal { internal sealed class Sort + where TKey : notnull { - private readonly IObservable> _source; - private readonly IComparer _comparer; - private readonly SortOptimisations _sortOptimisations; - private readonly IObservable> _comparerChangedObservable; - private readonly IObservable _resorter; + private readonly IComparer? _comparer; + + private readonly IObservable>? _comparerChangedObservable; private readonly int _resetThreshold; - public Sort(IObservable> source, - IComparer comparer, - SortOptimisations sortOptimisations = SortOptimisations.None, - IObservable> comparerChangedObservable = null, - IObservable resorter = null, - int resetThreshold = -1) + private readonly IObservable? _resorter; + + private readonly SortOptimisations _sortOptimisations; + + private readonly IObservable> _source; + + public Sort(IObservable> source, IComparer? comparer, SortOptimisations sortOptimisations = SortOptimisations.None, IObservable>? comparerChangedObservable = null, IObservable? resorter = null, int resetThreshold = -1) { - if (comparer == null && comparerChangedObservable == null) + if (comparer is null && comparerChangedObservable is null) { throw new ArgumentException("Must specify comparer or comparerChangedObservable"); } @@ -43,52 +42,47 @@ public Sort(IObservable> source, public IObservable> Run() { - return Observable.Create>(observer => - { - var sorter = new Sorter(_sortOptimisations, _comparer, _resetThreshold); - var locker = new object(); + return Observable.Create>( + observer => + { + var sorter = new Sorter(_sortOptimisations, _comparer, _resetThreshold); + var locker = new object(); - //check for nulls so we can prevent a lock when not required - if (_comparerChangedObservable == null && _resorter == null) - { - return _source - .Select(sorter.Sort) - .Where(result => result != null) - .SubscribeSafe(observer); - } + // check for nulls so we can prevent a lock when not required + if (_comparerChangedObservable is null && _resorter is null) + { + return _source.Select(sorter.Sort).Where(result => result is not null).Select(x => x!).SubscribeSafe(observer); + } - var comparerChanged = (_comparerChangedObservable ?? Observable.Never>()) - .Synchronize(locker).Select(sorter.Sort); + var comparerChanged = (_comparerChangedObservable ?? Observable.Never>()).Synchronize(locker).Select(sorter.Sort); - var sortAgain = (_resorter ?? Observable.Never()) - .Synchronize(locker).Select(_ => sorter.Sort()); + var sortAgain = (_resorter ?? Observable.Never()).Synchronize(locker).Select(_ => sorter.Sort()); - var dataChanged = _source.Synchronize(locker) - .Select(sorter.Sort); + var dataChanged = _source.Synchronize(locker).Select(sorter.Sort); - return comparerChanged - .Merge(dataChanged) - .Merge(sortAgain) - .Where(result => result != null) - .SubscribeSafe(observer); - }); + return comparerChanged.Merge(dataChanged).Merge(sortAgain).Where(result => result is not null).Select(x => x!).SubscribeSafe(observer); + }); } private class Sorter { - private readonly ChangeAwareCache _cache = new ChangeAwareCache(); + private readonly ChangeAwareCache _cache = new(); + private readonly SortOptimisations _optimisations; + private readonly int _resetThreshold; + private IndexCalculator? _calculator; + private KeyValueComparer _comparer; - private IKeyValueCollection _sorted = new KeyValueCollection(); - private bool _haveReceivedData ; + + private bool _haveReceivedData; + private bool _initialised; - private IndexCalculator _calculator; - public Sorter(SortOptimisations optimisations, - IComparer comparer = null, - int resetThreshold = -1) + private IKeyValueCollection _sorted = new KeyValueCollection(); + + public Sorter(SortOptimisations optimisations, IComparer? comparer = null, int resetThreshold = -1) { _optimisations = optimisations; _resetThreshold = resetThreshold; @@ -96,56 +90,52 @@ public Sorter(SortOptimisations optimisations, } /// - /// Sorts the specified changes. Will return null if there are no changes + /// Sorts the specified changes. Will return null if there are no changes. /// /// The changes. - /// - public ISortedChangeSet Sort(IChangeSet changes) + /// The sorted change set. + public ISortedChangeSet? Sort(IChangeSet changes) { return DoSort(SortReason.DataChanged, changes); } /// - /// Sorts all data using the specified comparer + /// Sorts all data using the specified comparer. /// /// The comparer. - /// - public ISortedChangeSet Sort(IComparer comparer) + /// The sorted change set. + public ISortedChangeSet? Sort(IComparer comparer) { _comparer = new KeyValueComparer(comparer); return DoSort(SortReason.ComparerChanged); } /// - /// Sorts all data using the current comparer + /// Sorts all data using the current comparer. /// - /// - public ISortedChangeSet Sort() + /// The sorted change set. + public ISortedChangeSet? Sort() { return DoSort(SortReason.Reorder); } /// - /// Sorts using the specified sorter. Will return null if there are no changes + /// Sorts using the specified sorter. Will return null if there are no changes. /// /// The sort reason. /// The changes. - /// - private ISortedChangeSet DoSort(SortReason sortReason, IChangeSet changes = null) + /// The sorted change set. + private ISortedChangeSet? DoSort(SortReason sortReason, IChangeSet? changes = null) { - if (changes != null) + if (changes is not null) { _cache.Clone(changes); changes = _cache.CaptureChanges(); _haveReceivedData = true; - if (_comparer == null) - { - return null; - } } - //if the comparer is not set, return nothing - if (_comparer == null || !_haveReceivedData) + // if the comparer is not set, return nothing + if (!_haveReceivedData) { return null; } @@ -155,32 +145,49 @@ private ISortedChangeSet DoSort(SortReason sortReason, IChangeSet sortReason = SortReason.InitialLoad; _initialised = true; } - else if (changes != null && (_resetThreshold > 0 && changes.Count >= _resetThreshold)) + else if (changes is not null && (_resetThreshold > 0 && changes.Count >= _resetThreshold)) { sortReason = SortReason.Reset; } - IChangeSet changeSet; + IChangeSet? changeSet; switch (sortReason) { case SortReason.InitialLoad: { - //For the first batch, changes may have arrived before the comparer was set. - //therefore infer the first batch of changes from the cache + // For the first batch, changes may have arrived before the comparer was set. + // therefore infer the first batch of changes from the cache _calculator = new IndexCalculator(_comparer, _optimisations); changeSet = _calculator.Load(_cache); } break; + case SortReason.Reset: { - _calculator.Reset(_cache); + if (_calculator is null) + { + throw new InvalidOperationException("The calculator has not been initialized"); + } + + _calculator?.Reset(_cache); changeSet = changes; } break; + case SortReason.DataChanged: { + if (_calculator is null) + { + throw new InvalidOperationException("The calculator has not been initialized"); + } + + if (changes is null) + { + throw new InvalidOperationException("Data has been indicated as changed, but changes is null."); + } + changeSet = _calculator.Calculate(changes); } @@ -188,6 +195,11 @@ private ISortedChangeSet DoSort(SortReason sortReason, IChangeSet case SortReason.ComparerChanged: { + if (_calculator is null) + { + throw new InvalidOperationException("The calculator has not been initialized"); + } + changeSet = _calculator.ChangeComparer(_comparer); if (_resetThreshold > 0 && _cache.Count >= _resetThreshold) { @@ -205,17 +217,31 @@ private ISortedChangeSet DoSort(SortReason sortReason, IChangeSet case SortReason.Reorder: { + if (_calculator is null) + { + throw new InvalidOperationException("The calculator has not been initialized"); + } + changeSet = _calculator.Reorder(); } break; + default: throw new ArgumentOutOfRangeException(nameof(sortReason)); } - Debug.Assert(changeSet != null, "changeSet != null"); - if ((sortReason == SortReason.InitialLoad || sortReason == SortReason.DataChanged) - && changeSet.Count == 0) + if (changeSet is null) + { + throw new InvalidOperationException("Change set must not be null."); + } + + if (_calculator is null) + { + throw new InvalidOperationException("The calculator has not been initialized"); + } + + if ((sortReason == SortReason.InitialLoad || sortReason == SortReason.DataChanged) && changeSet.Count == 0) { return null; } diff --git a/src/DynamicData/Cache/Internal/SpecifiedGrouper.cs b/src/DynamicData/Cache/Internal/SpecifiedGrouper.cs index 743a3973f..2b1b0f7af 100644 --- a/src/DynamicData/Cache/Internal/SpecifiedGrouper.cs +++ b/src/DynamicData/Cache/Internal/SpecifiedGrouper.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,14 +10,16 @@ namespace DynamicData.Cache.Internal { internal class SpecifiedGrouper + where TKey : notnull + where TGroupKey : notnull { - private readonly IObservable> _source; private readonly Func _groupSelector; + private readonly IObservable> _resultGroupSource; - public SpecifiedGrouper(IObservable> source, - Func groupSelector, - IObservable> resultGroupSource) + private readonly IObservable> _source; + + public SpecifiedGrouper(IObservable> source, Func groupSelector, IObservable> resultGroupSource) { _source = source ?? throw new ArgumentNullException(nameof(source)); _groupSelector = groupSelector ?? throw new ArgumentNullException(nameof(groupSelector)); @@ -26,66 +28,58 @@ public SpecifiedGrouper(IObservable> source, public IObservable> Run() { - return Observable.Create> - ( - observer => + return Observable.Create>( + observer => { var locker = new object(); - //create source group cache - var sourceGroups = _source.Synchronize(locker) - .Group(_groupSelector) - .DisposeMany() - .AsObservableCache(); + // create source group cache + var sourceGroups = _source.Synchronize(locker).Group(_groupSelector).DisposeMany().AsObservableCache(); - //create parent groups - var parentGroups = _resultGroupSource.Synchronize(locker) - .Transform(x => - { - //if child already has data, populate it. - var result = new ManagedGroup(x); - var child = sourceGroups.Lookup(x); - if (child.HasValue) + // create parent groups + var parentGroups = _resultGroupSource.Synchronize(locker).Transform( + x => { - //dodgy cast but fine as a groups is always a ManagedGroup; - var group = (ManagedGroup)child.Value; - result.Update(updater => updater.Clone(group.GetInitialUpdates())); - } + // if child already has data, populate it. + var result = new ManagedGroup(x); + var child = sourceGroups.Lookup(x); + if (child.HasValue) + { + // dodgy cast but fine as a groups is always a ManagedGroup; + var group = (ManagedGroup)child.Value; + result.Update(updater => updater.Clone(group.GetInitialUpdates())); + } - return result; - }) - .DisposeMany() - .AsObservableCache(); + return result; + }).DisposeMany().AsObservableCache(); - //connect to each individual item and update the resulting group - var updateFromcChilds = sourceGroups.Connect() - .SubscribeMany(x => x.Cache.Connect().Subscribe(updates => - { - var groupToUpdate = parentGroups.Lookup(x.Key); - if (groupToUpdate.HasValue) - { - groupToUpdate.Value.Update(updater => updater.Clone(updates)); - } - })) - .DisposeMany() - .Subscribe(); + // connect to each individual item and update the resulting group + var updatesFromChildren = sourceGroups.Connect().SubscribeMany( + x => x.Cache.Connect().Subscribe( + updates => + { + var groupToUpdate = parentGroups.Lookup(x.Key); + if (groupToUpdate.HasValue) + { + groupToUpdate.Value.Update(updater => updater.Clone(updates)); + } + })).DisposeMany().Subscribe(); - var notifier = parentGroups - .Connect() - .Select(x => - { - var groups = x.Select(s => new Change, TGroupKey>(s.Reason, s.Key, s.Current)); - return new GroupChangeSet(groups); - }) - .SubscribeSafe(observer); + var notifier = parentGroups.Connect().Select( + x => + { + var groups = x.Select(s => new Change, TGroupKey>(s.Reason, s.Key, s.Current)); + return new GroupChangeSet(groups); + }).SubscribeSafe(observer); - return Disposable.Create(() => - { - notifier.Dispose(); - sourceGroups.Dispose(); - parentGroups.Dispose(); - updateFromcChilds.Dispose(); - }); + return Disposable.Create( + () => + { + notifier.Dispose(); + sourceGroups.Dispose(); + parentGroups.Dispose(); + updatesFromChildren.Dispose(); + }); }); } } diff --git a/src/DynamicData/Cache/Internal/StaticFilter.cs b/src/DynamicData/Cache/Internal/StaticFilter.cs index 40069b9f9..c7a109812 100644 --- a/src/DynamicData/Cache/Internal/StaticFilter.cs +++ b/src/DynamicData/Cache/Internal/StaticFilter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,10 +8,12 @@ namespace DynamicData.Cache.Internal { internal class StaticFilter + where TKey : notnull { - private readonly IObservable> _source; private readonly Func _filter; + private readonly IObservable> _source; + public StaticFilter(IObservable> source, Func filter) { _source = source; @@ -20,18 +22,18 @@ public StaticFilter(IObservable> source, Func> Run() { - return _source.Scan((ChangeAwareCache)null, (cache, changes) => - { - if (cache == null) + return _source.Scan( + (ChangeAwareCache?)null, + (cache, changes) => { - cache = new ChangeAwareCache(changes.Count); - } + cache ??= new ChangeAwareCache(changes.Count); - cache.FilterChanges(changes, _filter); - return cache; - }) - .Select(cache => cache.CaptureChanges()) - .NotEmpty(); + cache.FilterChanges(changes, _filter); + return cache; + }) + .Where(x => x is not null) + .Select(x => x!) + .Select(cache => cache.CaptureChanges()).NotEmpty(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/StatusMonitor.cs b/src/DynamicData/Cache/Internal/StatusMonitor.cs index ef0aaa239..47257a99d 100644 --- a/src/DynamicData/Cache/Internal/StatusMonitor.cs +++ b/src/DynamicData/Cache/Internal/StatusMonitor.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,6 +6,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal @@ -21,54 +22,53 @@ public StatusMonitor(IObservable source) public IObservable Run() { - return Observable.Create(observer => - { - var statusSubject = new Subject(); - var status = ConnectionStatus.Pending; + return Observable.Create( + observer => + { + var statusSubject = new Subject(); + var status = ConnectionStatus.Pending; - void Error(Exception ex) - { - status = ConnectionStatus.Errored; - statusSubject.OnNext(status); - observer.OnError(ex); - } + void Error(Exception ex) + { + status = ConnectionStatus.Errored; + statusSubject.OnNext(status); + observer.OnError(ex); + } - void Completion() - { - if (status == ConnectionStatus.Errored) - { - return; - } + void Completion() + { + if (status == ConnectionStatus.Errored) + { + return; + } - status = ConnectionStatus.Completed; - statusSubject.OnNext(status); - } + status = ConnectionStatus.Completed; + statusSubject.OnNext(status); + } - void Updated() - { - if (status != ConnectionStatus.Pending) - { - return; - } + void Updated() + { + if (status != ConnectionStatus.Pending) + { + return; + } - status = ConnectionStatus.Loaded; - statusSubject.OnNext(status); - } + status = ConnectionStatus.Loaded; + statusSubject.OnNext(status); + } - var monitor = _source.Subscribe(_ => Updated(), Error, Completion); + var monitor = _source.Subscribe(_ => Updated(), Error, Completion); - var subscriber = statusSubject - .StartWith(status) - .DistinctUntilChanged() - .SubscribeSafe(observer); + var subscriber = statusSubject.StartWith(status).DistinctUntilChanged().SubscribeSafe(observer); - return Disposable.Create(() => - { - statusSubject.OnCompleted(); - monitor.Dispose(); - subscriber.Dispose(); - }); - }); + return Disposable.Create( + () => + { + statusSubject.OnCompleted(); + monitor.Dispose(); + subscriber.Dispose(); + }); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/SubscribeMany.cs b/src/DynamicData/Cache/Internal/SubscribeMany.cs index bebfc8acc..e559a2e48 100644 --- a/src/DynamicData/Cache/Internal/SubscribeMany.cs +++ b/src/DynamicData/Cache/Internal/SubscribeMany.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,19 +9,21 @@ namespace DynamicData.Cache.Internal { internal class SubscribeMany + where TKey : notnull { private readonly IObservable> _source; + private readonly Func _subscriptionFactory; public SubscribeMany(IObservable> source, Func subscriptionFactory) { - if (subscriptionFactory == null) + if (subscriptionFactory is null) { throw new ArgumentNullException(nameof(subscriptionFactory)); } _source = source ?? throw new ArgumentNullException(nameof(source)); - _subscriptionFactory = (t, key) => subscriptionFactory(t); + _subscriptionFactory = (t, _) => subscriptionFactory(t); } public SubscribeMany(IObservable> source, Func subscriptionFactory) @@ -32,21 +34,13 @@ public SubscribeMany(IObservable> source, Func> Run() { - - return Observable.Create> - ( - observer => + return Observable.Create>( + observer => { var published = _source.Publish(); - var subscriptions = published - .Transform((t, k) => _subscriptionFactory(t, k)) - .DisposeMany() - .Subscribe(); - - return new CompositeDisposable( - subscriptions, - published.SubscribeSafe(observer), - published.Connect()); + var subscriptions = published.Transform((t, k) => _subscriptionFactory(t, k)).DisposeMany().Subscribe(); + + return new CompositeDisposable(subscriptions, published.SubscribeSafe(observer), published.Connect()); }); } } diff --git a/src/DynamicData/Cache/Internal/Switch.cs b/src/DynamicData/Cache/Internal/Switch.cs index 99a23d31b..aa0cde2e4 100644 --- a/src/DynamicData/Cache/Internal/Switch.cs +++ b/src/DynamicData/Cache/Internal/Switch.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,6 +9,7 @@ namespace DynamicData.Cache.Internal { internal sealed class Switch + where TKey : notnull { private readonly IObservable>> _sources; @@ -19,27 +20,25 @@ public Switch(IObservable>> sources) public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); + return Observable.Create>( + observer => + { + var locker = new object(); - var destination = new LockFreeObservableCache(); + var destination = new LockFreeObservableCache(); - var populator = Observable.Switch(_sources - .Do(_ => - { - lock (locker) - { - destination.Clear(); - } - })) - .Synchronize(locker) - .PopulateInto(destination); + var populator = Observable.Switch( + _sources.Do( + _ => + { + lock (locker) + { + destination.Clear(); + } + })).Synchronize(locker).PopulateInto(destination); - return new CompositeDisposable(destination, - populator, - destination.Connect().SubscribeSafe(observer)); - }); + return new CompositeDisposable(destination, populator, destination.Connect().SubscribeSafe(observer)); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/TimeExpirer.cs b/src/DynamicData/Cache/Internal/TimeExpirer.cs index 4ba553d61..c3e5de8ea 100644 --- a/src/DynamicData/Cache/Internal/TimeExpirer.cs +++ b/src/DynamicData/Cache/Internal/TimeExpirer.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,121 +8,119 @@ using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class TimeExpirer + where TKey : notnull { - private readonly IObservable> _source; - private readonly Func _timeSelector; private readonly TimeSpan? _interval; + private readonly IScheduler _scheduler; - public TimeExpirer(IObservable> source, - Func timeSelector, - TimeSpan? interval, - IScheduler scheduler) + private readonly IObservable> _source; + + private readonly Func _timeSelector; + + public TimeExpirer(IObservable> source, Func timeSelector, TimeSpan? interval, IScheduler scheduler) { _source = source ?? throw new ArgumentNullException(nameof(source)); _timeSelector = timeSelector ?? throw new ArgumentNullException(nameof(timeSelector)); _interval = interval; - _scheduler = scheduler ?? Scheduler.Default; + _scheduler = scheduler; } - public IObservable>> ForExpiry() + public IObservable> ExpireAfter() { - return Observable.Create>>(observer => - { - var dateTime = DateTime.Now; - - var autoRemover = _source - .Do(x => dateTime = _scheduler.Now.DateTime) - .Transform((t, v) => - { - var removeAt = _timeSelector(t); - var expireAt = removeAt.HasValue ? dateTime.Add(removeAt.Value) : DateTime.MaxValue; - return new ExpirableItem(t, v, expireAt); - }) - .AsObservableCache(); - - void RemovalAction() - { - try + return Observable.Create>( + observer => { - var toRemove = autoRemover.KeyValues.Where(kv => kv.Value.ExpireAt <= _scheduler.Now.DateTime) - .ToList(); + var cache = new IntermediateCache(_source); - observer.OnNext(toRemove.Select(kv => new KeyValuePair(kv.Key, kv.Value.Value)).ToList()); - } - catch (Exception ex) - { - observer.OnError(ex); - } - } - - var removalSubscripion = new SingleAssignmentDisposable(); - if (_interval.HasValue) - { - // use polling - removalSubscripion.Disposable = _scheduler.ScheduleRecurringAction(_interval.Value, RemovalAction); - } - else - { - //create a timer for each distinct time - removalSubscripion.Disposable = autoRemover.Connect() - .DistinctValues(ei => ei.ExpireAt) - .SubscribeMany(datetime => - { - var expireAt = datetime.Subtract(_scheduler.Now.DateTime); - return Observable.Timer(expireAt, _scheduler) - .Take(1) - .Subscribe(_ => RemovalAction()); - }) - .Subscribe(); - } - - return Disposable.Create(() => - { - removalSubscripion.Dispose(); - autoRemover.Dispose(); - }); - }); + var published = cache.Connect().Publish(); + var subscriber = published.SubscribeSafe(observer); + + var autoRemover = published.ForExpiry(_timeSelector, _interval, _scheduler).Finally(observer.OnCompleted).Subscribe( + keys => + { + try + { + cache.Edit(updater => updater.Remove(keys.Select(kv => kv.Key))); + } + catch (Exception ex) + { + observer.OnError(ex); + } + }); + + var connected = published.Connect(); + + return Disposable.Create( + () => + { + connected.Dispose(); + subscriber.Dispose(); + autoRemover.Dispose(); + cache.Dispose(); + }); + }); } - public IObservable> ExpireAfter() + public IObservable>> ForExpiry() { - return Observable.Create>(observer => - { - var cache = new IntermediateCache(_source); + return Observable.Create>>( + observer => + { + var dateTime = DateTime.Now; - var published = cache.Connect().Publish(); - var subscriber = published.SubscribeSafe(observer); + var autoRemover = _source.Do(_ => dateTime = _scheduler.Now.DateTime).Transform( + (t, v) => + { + var removeAt = _timeSelector(t); + var expireAt = removeAt.HasValue ? dateTime.Add(removeAt.Value) : DateTime.MaxValue; + return new ExpirableItem(t, v, expireAt); + }).AsObservableCache(); - var autoRemover = published.ForExpiry(_timeSelector, _interval, _scheduler) - .Finally(observer.OnCompleted) - .Subscribe(keys => - { - try + void RemovalAction() { - cache.Edit(updater => updater.Remove(keys.Select(kv => kv.Key))); + try + { + var toRemove = autoRemover.KeyValues.Where(kv => kv.Value.ExpireAt <= _scheduler.Now.DateTime).ToList(); + + observer.OnNext(toRemove.Select(kv => new KeyValuePair(kv.Key, kv.Value.Value)).ToList()); + } + catch (Exception ex) + { + observer.OnError(ex); + } } - catch (Exception ex) + + var removalSubscription = new SingleAssignmentDisposable(); + if (_interval.HasValue) { - observer.OnError(ex); + // use polling + removalSubscription.Disposable = _scheduler.ScheduleRecurringAction(_interval.Value, RemovalAction); + } + else + { + // create a timer for each distinct time + removalSubscription.Disposable = autoRemover.Connect().DistinctValues(ei => ei.ExpireAt).SubscribeMany( + datetime => + { + var expireAt = datetime.Subtract(_scheduler.Now.DateTime); + return Observable.Timer(expireAt, _scheduler).Take(1).Subscribe(_ => RemovalAction()); + }).Subscribe(); } - }); - - var connected = published.Connect(); - return Disposable.Create(() => - { - connected.Dispose(); - subscriber.Dispose(); - autoRemover.Dispose(); - cache.Dispose(); - }); - }); + return Disposable.Create( + () => + { + removalSubscription.Dispose(); + autoRemover.Dispose(); + }); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/ToObservableChangeSet.cs b/src/DynamicData/Cache/Internal/ToObservableChangeSet.cs index 6e0b8123a..cd53e18b5 100644 --- a/src/DynamicData/Cache/Internal/ToObservableChangeSet.cs +++ b/src/DynamicData/Cache/Internal/ToObservableChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,35 +9,32 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class ToObservableChangeSet + where TKey : notnull { - private readonly IObservable> _source; + private readonly Func? _expireAfter; + private readonly Func _keySelector; - private readonly Func _expireAfter; + private readonly int _limitSizeTo; + private readonly IScheduler _scheduler; + private readonly bool _singleValueSource; - public ToObservableChangeSet(IObservable source, - Func keySelector, - Func expireAfter, - int limitSizeTo, - IScheduler scheduler = null) + private readonly IObservable> _source; + + public ToObservableChangeSet(IObservable source, Func keySelector, Func? expireAfter, int limitSizeTo, IScheduler? scheduler = null) : this(source.Select(t => new[] { t }), keySelector, expireAfter, limitSizeTo, scheduler, true) { } - public ToObservableChangeSet([NotNull] IObservable> source, - [NotNull] Func keySelector, - Func expireAfter, - int limitSizeTo, - IScheduler scheduler = null, - bool singleValueSource = false) + public ToObservableChangeSet(IObservable> source, Func keySelector, Func? expireAfter, int limitSizeTo, IScheduler? scheduler = null, bool singleValueSource = false) { _source = source ?? throw new ArgumentNullException(nameof(source)); _keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector)); @@ -49,105 +46,94 @@ public ToObservableChangeSet([NotNull] IObservable> source, public IObservable> Run() { - return Observable.Create>(observer => - { - long orderItemWasAdded = -1; - var locker = new object(); - - if (_expireAfter == null && _limitSizeTo < 1) - { - return _source.Scan(new ChangeAwareCache(), (state, latest) => + return Observable.Create>( + observer => { - if (latest is IList list) - { - //zero allocation enumerator - var elist = EnumerableIList.Create(list); - if (!_singleValueSource) - { - state.Remove(state.Keys.Except(elist.Select(_keySelector)).ToList()); - } - foreach (var item in elist) - { - state.AddOrUpdate(item, _keySelector(item)); - } - } - else - { - if (!_singleValueSource) - { - state.Remove(state.Keys.Except(latest.Select(_keySelector)).ToList()); - } - foreach (var item in latest) - { - state.AddOrUpdate(item, _keySelector(item)); - } - } - - return state; - }) - .Select(state => state.CaptureChanges()) - .SubscribeSafe(observer); - } - + long orderItemWasAdded = -1; + var locker = new object(); - var cache = new ChangeAwareCache, TKey>(); - var sizeLimited = _source.Synchronize(locker) - .Scan(cache, (state, latest) => - { - latest.Select(t => - { - var key = _keySelector(t); - return CreateExpirableItem(t, key, ref orderItemWasAdded); - }) - .ForEach(ei => cache.AddOrUpdate(ei, ei.Key)); - - if (_limitSizeTo > 0 && state.Count > _limitSizeTo) + if (_expireAfter is null && _limitSizeTo < 1) { - var toRemove = state.Count - _limitSizeTo; - - //remove oldest items - cache.KeyValues - .OrderBy(exp => exp.Value.Index) - .Take(toRemove) - .ForEach(ei => cache.Remove(ei.Key)); + return _source.Scan( + new ChangeAwareCache(), + (state, latest) => + { + if (latest is IList list) + { + // zero allocation enumerator + var enumerableList = EnumerableIList.Create(list); + if (!_singleValueSource) + { + state.Remove(state.Keys.Except(enumerableList.Select(_keySelector)).ToList()); + } + + foreach (var item in enumerableList) + { + state.AddOrUpdate(item, _keySelector(item)); + } + } + else + { + var enumerable = latest.ToList(); + if (!_singleValueSource) + { + state.Remove(state.Keys.Except(enumerable.Select(_keySelector)).ToList()); + } + + foreach (var item in enumerable) + { + state.AddOrUpdate(item, _keySelector(item)); + } + } + + return state; + }).Select(state => state.CaptureChanges()).SubscribeSafe(observer); } - return state; - }) - .Select(state => state.CaptureChanges()) - .Publish(); - - - - - var timeLimited = (_expireAfter == null ? Observable.Never, TKey>>() : sizeLimited) - .Filter(ei => ei.ExpireAt != DateTime.MaxValue) - .MergeMany(grouping => - { - var expireAt = grouping.ExpireAt.Subtract(_scheduler.Now.DateTime); - return Observable.Timer(expireAt, _scheduler).Select(_ => grouping); - }) - .Synchronize(locker) - .Select(item => - { - cache.Remove(item.Key); - return cache.CaptureChanges(); + var cache = new ChangeAwareCache, TKey>(); + var sizeLimited = _source.Synchronize(locker).Scan( + cache, + (state, latest) => + { + latest.Select( + t => + { + var key = _keySelector(t); + return CreateExpirableItem(t, key, ref orderItemWasAdded); + }).ForEach(ei => cache.AddOrUpdate(ei, ei.Key)); + + if (_limitSizeTo > 0 && state.Count > _limitSizeTo) + { + var toRemove = state.Count - _limitSizeTo; + + // remove oldest items + cache.KeyValues.OrderBy(exp => exp.Value.Index).Take(toRemove).ForEach(ei => cache.Remove(ei.Key)); + } + + return state; + }).Select(state => state.CaptureChanges()).Publish(); + + var timeLimited = (_expireAfter is null ? Observable.Never, TKey>>() : sizeLimited).Filter(ei => ei.ExpireAt != DateTime.MaxValue).MergeMany( + grouping => + { + var expireAt = grouping.ExpireAt.Subtract(_scheduler.Now.DateTime); + return Observable.Timer(expireAt, _scheduler).Select(_ => grouping); + }).Synchronize(locker).Select( + item => + { + cache.Remove(item.Key); + return cache.CaptureChanges(); + }); + + var publisher = sizeLimited.Merge(timeLimited).Cast(ei => ei.Value).NotEmpty().SubscribeSafe(observer); + + return new CompositeDisposable(publisher, sizeLimited.Connect()); }); - - var publisher = sizeLimited - .Merge(timeLimited) - .Cast(ei => ei.Value) - .NotEmpty() - .SubscribeSafe(observer); - - return new CompositeDisposable(publisher, sizeLimited.Connect()); - }); - } private ExpirableItem CreateExpirableItem(TObject item, TKey key, ref long orderItemWasAdded) { - //check whether expiry has been set for any items + // check whether expiry has been set for any items var dateTime = _scheduler.Now.DateTime; var removeAt = _expireAfter?.Invoke(item); var expireAt = removeAt.HasValue ? dateTime.Add(removeAt.Value) : DateTime.MaxValue; diff --git a/src/DynamicData/Cache/Internal/Transform.cs b/src/DynamicData/Cache/Internal/Transform.cs index b28dc3a7c..ea977c463 100644 --- a/src/DynamicData/Cache/Internal/Transform.cs +++ b/src/DynamicData/Cache/Internal/Transform.cs @@ -1,24 +1,26 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class Transform + where TKey : notnull { + private readonly Action>? _exceptionCallback; + private readonly IObservable> _source; + private readonly Func, TKey, TDestination> _transformFactory; - private readonly Action> _exceptionCallback; + private readonly bool _transformOnRefresh; - public Transform(IObservable> source, - Func, TKey, TDestination> transformFactory, - Action> exceptionCallback = null, - bool transformOnRefresh = false) + public Transform(IObservable> source, Func, TKey, TDestination> transformFactory, Action>? exceptionCallback = null, bool transformOnRefresh = false) { _source = source; _exceptionCallback = exceptionCallback; @@ -28,68 +30,70 @@ public Transform(IObservable> source, public IObservable> Run() { - return _source.Scan((ChangeAwareCache)null, (cache, changes) => - { - if (cache == null) + return _source.Scan( + (ChangeAwareCache?)null, + (cache, changes) => { - cache = new ChangeAwareCache(changes.Count); - } + cache ??= new ChangeAwareCache(changes.Count); - var concreteType = changes.ToConcreteType(); - foreach (var change in concreteType) - { - switch (change.Reason) + foreach (var change in changes.ToConcreteType()) { - case ChangeReason.Add: - case ChangeReason.Update: + switch (change.Reason) { - TDestination transformed; - if (_exceptionCallback != null) - { - try + case ChangeReason.Add: + case ChangeReason.Update: { - transformed = _transformFactory(change.Current, change.Previous, change.Key); - cache.AddOrUpdate(transformed, change.Key); + TDestination transformed; + if (_exceptionCallback is not null) + { + try + { + transformed = _transformFactory(change.Current, change.Previous, change.Key); + cache.AddOrUpdate(transformed, change.Key); + } + catch (Exception ex) + { + _exceptionCallback(new Error(ex, change.Current, change.Key)); + } + } + else + { + transformed = _transformFactory(change.Current, change.Previous, change.Key); + cache.AddOrUpdate(transformed, change.Key); + } } - catch (Exception ex) + + break; + + case ChangeReason.Remove: + cache.Remove(change.Key); + break; + + case ChangeReason.Refresh: { - _exceptionCallback(new Error(ex, change.Current, change.Key)); + if (_transformOnRefresh) + { + var transformed = _transformFactory(change.Current, change.Previous, change.Key); + cache.AddOrUpdate(transformed, change.Key); + } + else + { + cache.Refresh(change.Key); + } } - } - else - { - transformed = _transformFactory(change.Current, change.Previous, change.Key); - cache.AddOrUpdate(transformed, change.Key); - } - } - break; - case ChangeReason.Remove: - cache.Remove(change.Key); - break; - case ChangeReason.Refresh: - { - if (_transformOnRefresh) - { - var transformed = _transformFactory(change.Current, change.Previous, change.Key); - cache.AddOrUpdate(transformed, change.Key); - } - else - { - cache.Refresh(change.Key); - } - } + break; - break; - case ChangeReason.Moved: - //Do nothing ! - break; + case ChangeReason.Moved: + // Do nothing ! + break; + } } - } - return cache; - }) - .Select(cache => cache.CaptureChanges()) + return cache; + }) + .Where(x => x is not null) + .Select(cache => cache!.CaptureChanges()) .NotEmpty(); } } diff --git a/src/DynamicData/Cache/Internal/TransformAsync.cs b/src/DynamicData/Cache/Internal/TransformAsync.cs index c232d0e17..6e863c622 100644 --- a/src/DynamicData/Cache/Internal/TransformAsync.cs +++ b/src/DynamicData/Cache/Internal/TransformAsync.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,98 +6,73 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class TransformAsync + where TKey : notnull { + private readonly Action>? _exceptionCallback; + + private readonly IObservable>? _forceTransform; + private readonly IObservable> _source; + private readonly Func, TKey, Task> _transformFactory; - private readonly IObservable> _forceTransform; - private readonly Action> _exceptionCallback; - private readonly int _maximumConcurrency; - public TransformAsync(IObservable> source, Func, TKey, Task> transformFactory, Action> exceptionCallback, int maximumConcurrency = 1, IObservable> forceTransform = null) + public TransformAsync(IObservable> source, Func, TKey, Task> transformFactory, Action>? exceptionCallback, IObservable>? forceTransform = null) { _source = source; _exceptionCallback = exceptionCallback; _transformFactory = transformFactory; - _maximumConcurrency = maximumConcurrency; _forceTransform = forceTransform; } public IObservable> Run() { - return Observable.Create>(observer => - { - var cache = new ChangeAwareCache(); - var transformer = _source.SelectMany(changes => DoTransform(cache, changes)); - - if (_forceTransform != null) - { - var locker = new object(); - var forced = _forceTransform - .Synchronize(locker) - .SelectMany(shouldTransform => DoTransform(cache, shouldTransform)); - - transformer = transformer.Synchronize(locker).Merge(forced); - } - - return transformer.SubscribeSafe(observer); - }); + return Observable.Create>( + observer => + { + var cache = new ChangeAwareCache(); + var transformer = _source.SelectMany(changes => DoTransform(cache, changes)); + + if (_forceTransform is not null) + { + var locker = new object(); + var forced = _forceTransform.Synchronize(locker).SelectMany(shouldTransform => DoTransform(cache, shouldTransform)); + + transformer = transformer.Synchronize(locker).Merge(forced); + } + + return transformer.SubscribeSafe(observer); + }); } private async Task> DoTransform(ChangeAwareCache cache, Func shouldTransform) { - var toTransform = cache.KeyValues - .Where(kvp => shouldTransform(kvp.Value.Source, kvp.Key)) - .Select(kvp => new Change(ChangeReason.Update, kvp.Key, kvp.Value.Source, kvp.Value.Source)) - .ToArray(); + var toTransform = cache.KeyValues.Where(kvp => shouldTransform(kvp.Value.Source, kvp.Key)).Select(kvp => new Change(ChangeReason.Update, kvp.Key, kvp.Value.Source, kvp.Value.Source)).ToArray(); var transformed = await Task.WhenAll(toTransform.Select(Transform)).ConfigureAwait(false); return ProcessUpdates(cache, transformed); } - private async Task> DoTransform(ChangeAwareCache cache, IChangeSet changes ) + private async Task> DoTransform(ChangeAwareCache cache, IChangeSet changes) { var transformed = await Task.WhenAll(changes.Select(Transform)).ConfigureAwait(false); return ProcessUpdates(cache, transformed); } - private async Task Transform(Change change) - { - try - { - if (change.Reason == ChangeReason.Add || change.Reason == ChangeReason.Update) - { - var destination = await _transformFactory(change.Current, change.Previous, change.Key).ConfigureAwait(false); - return new TransformResult(change, new TransformedItemContainer(change.Current, destination)); - } - - return new TransformResult(change); - } - catch (Exception ex) - { - //only handle errors if a handler has been specified - if (_exceptionCallback != null) - { - return new TransformResult(change, ex); - } - - throw; - } - } - private IChangeSet ProcessUpdates(ChangeAwareCache cache, TransformResult[] transformedItems) { - //check for errors and callback if a handler has been specified + // check for errors and callback if a handler has been specified var errors = transformedItems.Where(t => !t.Success).ToArray(); - if (errors.Any()) + if (errors.Length > 0) { - errors.ForEach(t => _exceptionCallback(new Error(t.Error, t.Change.Current, t.Change.Key))); + errors.ForEach(t => _exceptionCallback?.Invoke(new Error(t.Error, t.Change.Current, t.Change.Key))); } foreach (var result in transformedItems.Where(t => t.Success)) @@ -121,24 +96,50 @@ private IChangeSet ProcessUpdates(ChangeAwareCache new Change(change.Reason, - change.Key, - change.Current.Destination, - change.Previous.Convert(x => x.Destination), - change.CurrentIndex, - change.PreviousIndex)); + var transformed = changes.Select(change => new Change(change.Reason, change.Key, change.Current.Destination, change.Previous.Convert(x => x.Destination), change.CurrentIndex, change.PreviousIndex)); return new ChangeSet(transformed); } - private sealed class TransformResult + private async Task Transform(Change change) { - public Change Change { get; } - public Exception Error { get; } - public bool Success { get; } - public Optional Container { get; } - public TKey Key { get; } + try + { + if (change.Reason == ChangeReason.Add || change.Reason == ChangeReason.Update) + { + var destination = await _transformFactory(change.Current, change.Previous, change.Key).ConfigureAwait(false); + return new TransformResult(change, new TransformedItemContainer(change.Current, destination)); + } + return new TransformResult(change); + } + catch (Exception ex) + { + // only handle errors if a handler has been specified + if (_exceptionCallback is not null) + { + return new TransformResult(change, ex); + } + + throw; + } + } + + private readonly struct TransformedItemContainer + { + public TransformedItemContainer(TSource source, TDestination destination) + { + Source = source; + Destination = destination; + } + + public TSource Source { get; } + + public TDestination Destination { get; } + } + + private sealed class TransformResult + { public TransformResult(Change change, TransformedItemContainer container) { Change = change; @@ -162,18 +163,16 @@ public TransformResult(Change change, Exception error) Success = false; Key = change.Key; } - } - private readonly struct TransformedItemContainer - { - public TSource Source { get; } - public TDestination Destination { get; } + public Change Change { get; } - public TransformedItemContainer(TSource source, TDestination destination) - { - Source = source; - Destination = destination; - } + public Optional Container { get; } + + public Exception? Error { get; } + + public TKey Key { get; } + + public bool Success { get; } } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/TransformMany.cs b/src/DynamicData/Cache/Internal/TransformMany.cs index 5d7223d98..18a4e9884 100644 --- a/src/DynamicData/Cache/Internal/TransformMany.cs +++ b/src/DynamicData/Cache/Internal/TransformMany.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,59 +9,65 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Binding; using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class TransformMany + where TSourceKey : notnull + where TDestinationKey : notnull { - private readonly IObservable> _source; - private readonly Func>> _childChanges; - private readonly Func> _manySelector; + private readonly Func>>? _childChanges; + private readonly Func _keySelector; - public TransformMany(IObservable> source, - Func> manySelector, - Func keySelector) - : this(source, manySelector, keySelector, t => Observable.Defer(() => - { - var subsequentChanges = manySelector(t).ToObservableChangeSet(keySelector); + private readonly Func> _manySelector; - if (manySelector(t).Count > 0) - { - return subsequentChanges; - } + private readonly IObservable> _source; + + public TransformMany(IObservable> source, Func> manySelector, Func keySelector) + : this( + source, + manySelector, + keySelector, + t => Observable.Defer( + () => + { + var subsequentChanges = manySelector(t).ToObservableChangeSet(keySelector); - return Observable.Return(ChangeSet.Empty) - .Concat(subsequentChanges); - })) + if (manySelector(t).Count > 0) + { + return subsequentChanges; + } + return Observable.Return(ChangeSet.Empty).Concat(subsequentChanges); + })) { } - public TransformMany(IObservable> source, - Func> manySelector, - Func keySelector) - :this(source, manySelector, keySelector, t => Observable.Defer(() => - { - var subsequentChanges = manySelector(t).ToObservableChangeSet(keySelector); + public TransformMany(IObservable> source, Func> manySelector, Func keySelector) + : this( + source, + manySelector, + keySelector, + t => Observable.Defer( + () => + { + var subsequentChanges = manySelector(t).ToObservableChangeSet(keySelector); - if (manySelector(t).Count > 0) - { - return subsequentChanges; - } + if (manySelector(t).Count > 0) + { + return subsequentChanges; + } - return Observable.Return(ChangeSet.Empty) - .Concat(subsequentChanges); - })) + return Observable.Return(ChangeSet.Empty).Concat(subsequentChanges); + })) { } - public TransformMany(IObservable> source, - Func> manySelector, - Func keySelector, - Func>> childChanges = null) + public TransformMany(IObservable> source, Func> manySelector, Func keySelector, Func>>? childChanges = null) { _source = source; _manySelector = manySelector; @@ -71,64 +77,102 @@ public TransformMany(IObservable> source, public IObservable> Run() { - return _childChanges == null - ? Create() - : CreateWithChangeset(); + return _childChanges is null ? Create() : CreateWithChangeSet(); } private IObservable> Create() { - return _source.Transform((t, key) => - { - var destination = _manySelector(t) - // ReSharper disable once InconsistentlySynchronizedField - .Select(m => new DestinationContainer(m, _keySelector(m))) - .ToArray(); - return new ManyContainer(() => destination); - }, true) - .Select(changes => new ChangeSet(new DestinationEnumerator(changes))); + return _source.Transform( + (t, _) => + { + var destination = _manySelector(t).Select(m => new DestinationContainer(m, _keySelector(m))).ToArray(); + return new ManyContainer(() => destination); + }, + true).Select(changes => new ChangeSet(new DestinationEnumerator(changes))); } - private IObservable> CreateWithChangeset() + private IObservable> CreateWithChangeSet() { - return Observable.Create>(observer => + if (_childChanges is null) { - var result = new ChangeAwareCache(); + throw new InvalidOperationException("The childChanges is null and should not be."); + } - var transformed = _source.Transform((t, key) => - { - //Only skip initial for first time Adds where there is initial data records - var locker = new object(); - var collection = _manySelector(t); - var changes = _childChanges(t).Synchronize(locker).Skip(1); - return new ManyContainer(() => + return Observable.Create>( + observer => { - lock (locker) - { - return collection - .Select(m => new DestinationContainer(m, _keySelector(m))) - .ToArray(); - } - }, changes); - }).Publish(); - - var outerLock = new object(); - var initial = transformed - .Synchronize(outerLock) - .Select(changes => new ChangeSet(new DestinationEnumerator(changes))); - - var subsequent = transformed - .MergeMany(x => x.Changes) - .Synchronize(outerLock); - - var allChanges = initial.Merge(subsequent).Select(changes => + var result = new ChangeAwareCache(); + + var transformed = _source.Transform( + (t, _) => + { + // Only skip initial for first time Adds where there is initial data records + var locker = new object(); + var collection = _manySelector(t); + var changes = _childChanges(t).Synchronize(locker).Skip(1); + return new ManyContainer( + () => + { + lock (locker) + { + return collection.Select(m => new DestinationContainer(m, _keySelector(m))).ToArray(); + } + }, + changes); + }).Publish(); + + var outerLock = new object(); + var initial = transformed.Synchronize(outerLock).Select(changes => new ChangeSet(new DestinationEnumerator(changes))); + + var subsequent = transformed.MergeMany(x => x.Changes).Synchronize(outerLock); + + var allChanges = initial.Merge(subsequent).Select( + changes => + { + result.Clone(changes); + return result.CaptureChanges(); + }); + + return new CompositeDisposable(allChanges.SubscribeSafe(observer), transformed.Connect()); + }); + } + + private sealed class DestinationContainer + { + public DestinationContainer(TDestination item, TDestinationKey key) + { + Item = item; + Key = key; + } + + public static IEqualityComparer KeyComparer { get; } = new KeyEqualityComparer(); + + public TDestination Item { get; } + + public TDestinationKey Key { get; } + + private sealed class KeyEqualityComparer : IEqualityComparer + { + public bool Equals(DestinationContainer? x, DestinationContainer? y) { - result.Clone(changes); - return result.CaptureChanges(); - }); + if (x is null && y is null) + { + return true; + } - return new CompositeDisposable(allChanges.SubscribeSafe(observer), transformed.Connect()); - }); + if (x is null || y is null) + { + return false; + } + + return EqualityComparer.Default.Equals(x.Key, y.Key); + } + + public int GetHashCode(DestinationContainer obj) + { + return EqualityComparer.Default.GetHashCode(obj.Key); + } + } } private sealed class DestinationEnumerator : IEnumerable> @@ -181,7 +225,7 @@ public IEnumerator> GetEnumerator() var current = currentItems.First(d => d.Key.Equals(destination.Key)); var previous = previousItems.First(d => d.Key.Equals(destination.Key)); - //Do not update is items are the same reference + // Do not update is items are the same reference if (!ReferenceEquals(current.Item, previous.Item)) { yield return new Change(ChangeReason.Update, destination.Key, current.Item, previous.Item); @@ -203,65 +247,16 @@ IEnumerator IEnumerable.GetEnumerator() private sealed class ManyContainer { private readonly Func> _initial; - public IObservable> Changes { get; } - public IEnumerable Destination => _initial(); - public ManyContainer(Func> initial, IObservable> changes = null) + public ManyContainer(Func> initial, IObservable>? changes = null) { _initial = initial; - Changes = changes; - } - } - - private sealed class DestinationContainer - { - public TDestination Item { get; } - public TDestinationKey Key { get; } - - public DestinationContainer(TDestination item, TDestinationKey key) - { - Item = item; - Key = key; + Changes = changes ?? Observable.Empty>(); } - #region Equality Comparer - - private sealed class KeyEqualityComparer : IEqualityComparer - { - public bool Equals(DestinationContainer x, DestinationContainer y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null) - { - return false; - } - - if (ReferenceEquals(y, null)) - { - return false; - } - - if (x.GetType() != y.GetType()) - { - return false; - } - - return EqualityComparer.Default.Equals(x.Key, y.Key); - } - - public int GetHashCode(DestinationContainer obj) - { - return EqualityComparer.Default.GetHashCode(obj.Key); - } - } - - public static IEqualityComparer KeyComparer { get; } = new KeyEqualityComparer(); + public IObservable> Changes { get; } - #endregion + public IEnumerable Destination => _initial(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/TransformWithForcedTransform.cs b/src/DynamicData/Cache/Internal/TransformWithForcedTransform.cs index 799d76e03..6e4767d5f 100644 --- a/src/DynamicData/Cache/Internal/TransformWithForcedTransform.cs +++ b/src/DynamicData/Cache/Internal/TransformWithForcedTransform.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,21 +6,23 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal sealed class TransformWithForcedTransform + where TKey : notnull { + private readonly Action>? _exceptionCallback; + + private readonly IObservable> _forceTransform; + private readonly IObservable> _source; + private readonly Func, TKey, TDestination> _transformFactory; - private readonly IObservable> _forceTransform; - private readonly Action> _exceptionCallback; - public TransformWithForcedTransform(IObservable> source, - Func, TKey, TDestination> transformFactory, - IObservable> forceTransform, - Action> exceptionCallback = null) + public TransformWithForcedTransform(IObservable> source, Func, TKey, TDestination> transformFactory, IObservable> forceTransform, Action>? exceptionCallback = null) { _source = source; _exceptionCallback = exceptionCallback; @@ -30,38 +32,35 @@ public TransformWithForcedTransform(IObservable> sourc public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); - var shared = _source.Synchronize(locker).Publish(); - - //capture all items so we can apply a forced transform - var cache = new Cache(); - var cacheLoader = shared.Subscribe(changes => cache.Clone(changes)); + return Observable.Create>( + observer => + { + var locker = new object(); + var shared = _source.Synchronize(locker).Publish(); - //create change set of items where force refresh is applied - var refresher = _forceTransform.Synchronize(locker) - .Select(selector => CaptureChanges(cache, selector)) - .Select(changes => new ChangeSet(changes)) - .NotEmpty(); + // capture all items so we can apply a forced transform + var cache = new Cache(); + var cacheLoader = shared.Subscribe(changes => cache.Clone(changes)); - var sourceAndRefreshes = shared.Merge(refresher); + // create change set of items where force refresh is applied + var refresher = _forceTransform.Synchronize(locker).Select(selector => CaptureChanges(cache, selector)).Select(changes => new ChangeSet(changes)).NotEmpty(); - //do raw transform - var transform = new Transform(sourceAndRefreshes, _transformFactory, _exceptionCallback, true).Run(); + var sourceAndRefreshes = shared.Merge(refresher); - return new CompositeDisposable(cacheLoader,transform.SubscribeSafe(observer), shared.Connect()); - }); + // do raw transform + var transform = new Transform(sourceAndRefreshes, _transformFactory, _exceptionCallback, true).Run(); + return new CompositeDisposable(cacheLoader, transform.SubscribeSafe(observer), shared.Connect()); + }); } - private static IEnumerable> CaptureChanges(Cache cache, Func shouldTransform) + private static IEnumerable> CaptureChanges(Cache cache, Func shouldTransform) { foreach (var kvp in cache.KeyValues) { if (shouldTransform(kvp.Value, kvp.Key)) { - yield return new Change(ChangeReason.Refresh, kvp.Key,kvp.Value); + yield return new Change(ChangeReason.Refresh, kvp.Key, kvp.Value); } } } diff --git a/src/DynamicData/Cache/Internal/TreeBuilder.cs b/src/DynamicData/Cache/Internal/TreeBuilder.cs index 3205d73db..003067f78 100644 --- a/src/DynamicData/Cache/Internal/TreeBuilder.cs +++ b/src/DynamicData/Cache/Internal/TreeBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,227 +8,222 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.Cache.Internal { internal class TreeBuilder + where TKey : notnull where TObject : class { - private readonly IObservable> _source; private readonly Func _pivotOn; + private readonly IObservable, bool>> _predicateChanged; - public TreeBuilder([NotNull] IObservable> source, [NotNull] Func pivotOn, IObservable, bool>> predicateChanged) + private readonly IObservable> _source; + + public TreeBuilder(IObservable> source, Func pivotOn, IObservable, bool>>? predicateChanged) { _source = source ?? throw new ArgumentNullException(nameof(source)); _pivotOn = pivotOn ?? throw new ArgumentNullException(nameof(pivotOn)); _predicateChanged = predicateChanged ?? Observable.Return(DefaultPredicate); } - private static readonly Func, bool> DefaultPredicate = node => node.IsRoot; + private static Func, bool> DefaultPredicate => node => node.IsRoot; public IObservable, TKey>> Run() { - return Observable.Create, TKey>>(observer => - { - var locker = new object(); - var refilterObservable = new BehaviorSubject(Unit.Default); - - var allData = _source.Synchronize(locker).AsObservableCache(); - - //for each object we need a node which provides - //a structure to set the parent and children - var allNodes = allData.Connect() - .Synchronize(locker) - .Transform((t, v) => new Node(t, v)) - .AsObservableCache(); - - var groupedByPivot = allNodes.Connect() - .Synchronize(locker) - .Group(x => _pivotOn(x.Item)) - .AsObservableCache(); - - void UpdateChildren(Node parentNode) - { - var lookup = groupedByPivot.Lookup(parentNode.Key); - if (lookup.HasValue) + return Observable.Create, TKey>>( + observer => { - var children = lookup.Value.Cache.Items; - parentNode.Update(u => u.AddOrUpdate(children)); - children.ForEach(x => x.Parent = parentNode); - } - } - - //as nodes change, maintain parent and children - var parentSetter = allNodes.Connect() - .Do(changes => - { - var grouped = changes.GroupBy(c => _pivotOn(c.Current.Item)); - - foreach (var group in grouped) - { - var parentKey = group.Key; - var parent = allNodes.Lookup(parentKey); - - if (!parent.HasValue) - { - //deal with items which have no parent - foreach (var change in group) - { - if (change.Reason != ChangeReason.Refresh) - { - change.Current.Parent = null; - } - - switch (change.Reason) - { - case ChangeReason.Add: - UpdateChildren(change.Current); - break; - case ChangeReason.Update: - { - //copy children to the new node amd set parent - var children = change.Previous.Value.Children.Items; - change.Current.Update(updater => updater.AddOrUpdate(children)); - children.ForEach(child => child.Parent = change.Current); - - //remove from old parent if different - var previous = change.Previous.Value; - var previousParent = _pivotOn(previous.Item); - - if (!previousParent.Equals(previous.Key)) - { - allNodes.Lookup(previousParent) - .IfHasValue(n => { n.Update(u => u.Remove(change.Key)); }); - } - - break; - } - - case ChangeReason.Remove: - { - //remove children and null out parent - var children = change.Current.Children.Items; - change.Current.Update(updater => updater.Remove(children)); - children.ForEach(child => child.Parent = null); - - break; - } - - case ChangeReason.Refresh: - { - var previousParent = change.Current.Parent; - if (!previousParent.Equals(parent)) - { - previousParent.IfHasValue(n => n.Update(u => u.Remove(change.Key))); - change.Current.Parent = null; - } - - break; - } - } - } - } - else - { - //deal with items have a parent - parent.Value.Update(updater => - { - var p = parent.Value; - - foreach (var change in group) - { - var previous = change.Previous; - var node = change.Current; - var key = node.Key; - - switch (change.Reason) - { - case ChangeReason.Add: - { - // update the parent node - node.Parent = p; - updater.AddOrUpdate(node); - UpdateChildren(node); - - break; - } - - case ChangeReason.Update: - { - //copy children to the new node amd set parent - var children = previous.Value.Children.Items; - change.Current.Update(u => u.AddOrUpdate(children)); - children.ForEach(child => child.Parent = change.Current); - - //check whether the item has a new parent - var previousItem = previous.Value.Item; - var previousKey = previous.Value.Key; - var previousParent = _pivotOn(previousItem); - - if (!previousParent.Equals(previousKey)) - { - allNodes.Lookup(previousParent) - .IfHasValue(n => { n.Update(u => u.Remove(key)); }); - } - - //finally update the parent - node.Parent = p; - updater.AddOrUpdate(node); - - break; - } - - case ChangeReason.Remove: - { - node.Parent = null; - updater.Remove(key); - - var children = node.Children.Items; - change.Current.Update(u => u.Remove(children)); - children.ForEach(child => child.Parent = null); - - break; - } - - case ChangeReason.Refresh: - { - var previousParent = change.Current.Parent; - if (!previousParent.Equals(parent)) - { - previousParent.IfHasValue(n => n.Update(u => u.Remove(change.Key))); - change.Current.Parent = p; - updater.AddOrUpdate(change.Current); - } - - break; - } - } - } - }); - } - } - - refilterObservable.OnNext(Unit.Default); - }) - .DisposeMany() - .Subscribe(); - - var filter = _predicateChanged.Synchronize(locker).CombineLatest(refilterObservable, (predicate, _) => predicate); - var result = allNodes.Connect().Filter(filter).SubscribeSafe(observer); - - return Disposable.Create(() => - { - result.Dispose(); - parentSetter.Dispose(); - allData.Dispose(); - allNodes.Dispose(); - groupedByPivot.Dispose(); - refilterObservable.OnCompleted(); - }); - }); + var locker = new object(); + var reFilterObservable = new BehaviorSubject(Unit.Default); + + var allData = _source.Synchronize(locker).AsObservableCache(); + + // for each object we need a node which provides + // a structure to set the parent and children + var allNodes = allData.Connect().Synchronize(locker).Transform((t, v) => new Node(t, v)).AsObservableCache(); + + var groupedByPivot = allNodes.Connect().Synchronize(locker).Group(x => _pivotOn(x.Item)).AsObservableCache(); + + void UpdateChildren(Node parentNode) + { + var lookup = groupedByPivot.Lookup(parentNode.Key); + if (lookup.HasValue && lookup.Value is not null) + { + var children = lookup.Value.Cache.Items; + parentNode.Update(u => u.AddOrUpdate(children)); + children.ForEach(x => x.Parent = parentNode); + } + } + + // as nodes change, maintain parent and children + var parentSetter = allNodes.Connect().Do( + changes => + { + foreach (var group in changes.GroupBy(c => _pivotOn(c.Current.Item))) + { + var parentKey = group.Key; + var parent = allNodes.Lookup(parentKey); + + if (!parent.HasValue) + { + // deal with items which have no parent + foreach (var change in group) + { + if (change.Reason != ChangeReason.Refresh) + { + change.Current.Parent = null; + } + + switch (change.Reason) + { + case ChangeReason.Add: + UpdateChildren(change.Current); + break; + + case ChangeReason.Update: + { + // copy children to the new node amd set parent + var children = change.Previous.Value.Children.Items; + change.Current.Update(updater => updater.AddOrUpdate(children)); + children.ForEach(child => child.Parent = change.Current); + + // remove from old parent if different + var previous = change.Previous.Value; + var previousParent = _pivotOn(previous.Item); + + if (previousParent is not null && !previousParent.Equals(previous.Key)) + { + allNodes.Lookup(previousParent).IfHasValue(n => n.Update(u => u.Remove(change.Key))); + } + + break; + } + + case ChangeReason.Remove: + { + // remove children and null out parent + var children = change.Current.Children.Items; + change.Current.Update(updater => updater.Remove(children)); + children.ForEach(child => child.Parent = null); + + break; + } + + case ChangeReason.Refresh: + { + var previousParent = change.Current.Parent; + if (!previousParent.Equals(parent)) + { + previousParent.IfHasValue(n => n.Update(u => u.Remove(change.Key))); + change.Current.Parent = null; + } + + break; + } + } + } + } + else + { + // deal with items have a parent + parent.Value.Update( + updater => + { + var p = parent.Value; + + foreach (var change in group) + { + var previous = change.Previous; + var node = change.Current; + var key = node.Key; + + switch (change.Reason) + { + case ChangeReason.Add: + { + // update the parent node + node.Parent = p; + updater.AddOrUpdate(node); + UpdateChildren(node); + + break; + } + + case ChangeReason.Update: + { + // copy children to the new node amd set parent + var children = previous.Value.Children.Items; + change.Current.Update(u => u.AddOrUpdate(children)); + children.ForEach(child => child.Parent = change.Current); + + // check whether the item has a new parent + var previousItem = previous.Value.Item; + var previousKey = previous.Value.Key; + var previousParent = _pivotOn(previousItem); + + if (previousParent is not null && !previousParent.Equals(previousKey)) + { + allNodes.Lookup(previousParent).IfHasValue(n => n.Update(u => u.Remove(key))); + } + + // finally update the parent + node.Parent = p; + updater.AddOrUpdate(node); + + break; + } + + case ChangeReason.Remove: + { + node.Parent = null; + updater.Remove(key); + + var children = node.Children.Items; + change.Current.Update(u => u.Remove(children)); + children.ForEach(child => child.Parent = null); + + break; + } + + case ChangeReason.Refresh: + { + var previousParent = change.Current.Parent; + if (!previousParent.Equals(parent)) + { + previousParent.IfHasValue(n => n.Update(u => u.Remove(change.Key))); + change.Current.Parent = p; + updater.AddOrUpdate(change.Current); + } + + break; + } + } + } + }); + } + } + + reFilterObservable.OnNext(Unit.Default); + }).DisposeMany().Subscribe(); + + var filter = _predicateChanged.Synchronize(locker).CombineLatest(reFilterObservable, (predicate, _) => predicate); + var result = allNodes.Connect().Filter(filter).SubscribeSafe(observer); + + return Disposable.Create( + () => + { + result.Dispose(); + parentSetter.Dispose(); + allData.Dispose(); + allNodes.Dispose(); + groupedByPivot.Dispose(); + reFilterObservable.OnCompleted(); + }); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/TrueFor.cs b/src/DynamicData/Cache/Internal/TrueFor.cs index b8a06c804..80adb295c 100644 --- a/src/DynamicData/Cache/Internal/TrueFor.cs +++ b/src/DynamicData/Cache/Internal/TrueFor.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,14 +10,15 @@ namespace DynamicData.Cache.Internal { internal class TrueFor + where TKey : notnull { - private readonly IObservable> _source; - private readonly Func> _observableSelector; private readonly Func>, bool> _collectionMatcher; - public TrueFor(IObservable> source, - Func> observableSelector, - Func>, bool> collectionMatcher) + private readonly Func> _observableSelector; + + private readonly IObservable> _source; + + public TrueFor(IObservable> source, Func> observableSelector, Func>, bool> collectionMatcher) { _source = source ?? throw new ArgumentNullException(nameof(source)); _observableSelector = observableSelector ?? throw new ArgumentNullException(nameof(observableSelector)); @@ -26,19 +27,18 @@ public TrueFor(IObservable> source, public IObservable Run() { - return Observable.Create(observer => - { - var transformed = _source.Transform(t => new ObservableWithValue(t, _observableSelector(t))).Publish(); - var inlineChanges = transformed.MergeMany(t => t.Observable); - var queried = transformed.ToCollection(); - - //nb: we do not care about the inline change because we are only monitoring it to cause a re-evalutaion of all items - var publisher = queried.CombineLatest(inlineChanges, (items, inline) => _collectionMatcher(items)) - .DistinctUntilChanged() - .SubscribeSafe(observer); - - return new CompositeDisposable(publisher, transformed.Connect()); - }); + return Observable.Create( + observer => + { + var transformed = _source.Transform(t => new ObservableWithValue(t, _observableSelector(t))).Publish(); + var inlineChanges = transformed.MergeMany(t => t.Observable); + var queried = transformed.ToCollection(); + + // nb: we do not care about the inline change because we are only monitoring it to cause a re-evaluation of all items + var publisher = queried.CombineLatest(inlineChanges, (items, _) => _collectionMatcher(items)).DistinctUntilChanged().SubscribeSafe(observer); + + return new CompositeDisposable(publisher, transformed.Connect()); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/Cache/Internal/Virtualiser.cs b/src/DynamicData/Cache/Internal/Virtualise.cs similarity index 65% rename from src/DynamicData/Cache/Internal/Virtualiser.cs rename to src/DynamicData/Cache/Internal/Virtualise.cs index ed77e38cf..c2bb0cd1c 100644 --- a/src/DynamicData/Cache/Internal/Virtualiser.cs +++ b/src/DynamicData/Cache/Internal/Virtualise.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,12 +9,13 @@ namespace DynamicData.Cache.Internal { internal sealed class Virtualise + where TKey : notnull { private readonly IObservable> _source; + private readonly IObservable _virtualRequests; - public Virtualise(IObservable> source, - IObservable virtualRequests) + public Virtualise(IObservable> source, IObservable virtualRequests) { _source = source ?? throw new ArgumentNullException(nameof(source)); _virtualRequests = virtualRequests ?? throw new ArgumentNullException(nameof(virtualRequests)); @@ -22,34 +23,43 @@ public Virtualise(IObservable> source, public IObservable> Run() { - return Observable.Create>(observer => - { - var virtualiser = new Virtualiser(); - var locker = new object(); - - var request = _virtualRequests.Synchronize(locker).Select(virtualiser.Virtualise); - var datachange = _source.Synchronize(locker).Select(virtualiser.Update); - return request.Merge(datachange) - .Where(updates => updates != null) - .SubscribeSafe(observer); - }); + return Observable.Create>( + observer => + { + var virtualiser = new Virtualiser(); + var locker = new object(); + + var request = _virtualRequests.Synchronize(locker).Select(virtualiser.Virtualise).Where(x => x is not null).Select(x => x!); + var dataChange = _source.Synchronize(locker).Select(virtualiser.Update).Where(x => x is not null).Select(x => x!); + return request.Merge(dataChange).Where(updates => updates is not null).SubscribeSafe(observer); + }); } private sealed class Virtualiser { private IKeyValueCollection _all = new KeyValueCollection(); + private IKeyValueCollection _current = new KeyValueCollection(); - private IVirtualRequest _parameters; + private bool _isLoaded; - public Virtualiser(VirtualRequest request = null) + private IVirtualRequest _parameters; + + public Virtualiser(VirtualRequest? request = null) { _parameters = request ?? new VirtualRequest(); } - public IVirtualChangeSet Virtualise(IVirtualRequest parameters) + public IVirtualChangeSet? Update(ISortedChangeSet updates) { - if (parameters == null || parameters.StartIndex < 0 || parameters.Size < 1) + _isLoaded = true; + _all = updates.SortedItems; + return Virtualise(updates); + } + + public IVirtualChangeSet? Virtualise(IVirtualRequest? parameters) + { + if (parameters is null || parameters.StartIndex < 0 || parameters.Size < 1) { return null; } @@ -63,28 +73,19 @@ public IVirtualChangeSet Virtualise(IVirtualRequest parameters) return Virtualise(); } - public IVirtualChangeSet Update(ISortedChangeSet updates) - { - _isLoaded = true; - _all = updates.SortedItems; - return Virtualise(updates); - } - - private IVirtualChangeSet Virtualise(ISortedChangeSet updates = null) + private IVirtualChangeSet? Virtualise(ISortedChangeSet? updates = null) { - if (_isLoaded == false) + if (!_isLoaded) { return null; } var previous = _current; - var virtualised = _all.Skip(_parameters.StartIndex) - .Take(_parameters.Size) - .ToList(); + var virtualised = _all.Skip(_parameters.StartIndex).Take(_parameters.Size).ToList(); _current = new KeyValueCollection(virtualised, _all.Comparer, updates?.SortedItems.SortReason ?? SortReason.DataChanged, _all.Optimisations); - ////check for changes within the current virtualised page. Notify if there have been changes or if the overall count has changed + // check for changes within the current virtualised page. Notify if there have been changes or if the overall count has changed var notifications = FilteredIndexCalculator.Calculate(_current, previous, updates); if (notifications.Count == 0 && (previous.Count != _current.Count)) { diff --git a/src/DynamicData/Cache/MissingKeyException.cs b/src/DynamicData/Cache/MissingKeyException.cs index 119c9e6a3..0f17200d7 100644 --- a/src/DynamicData/Cache/MissingKeyException.cs +++ b/src/DynamicData/Cache/MissingKeyException.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,7 +8,7 @@ namespace DynamicData { /// - /// Thrown when a key is expected in a cache but not found + /// Thrown when a key is expected in a cache but not found. /// [Serializable] public class MissingKeyException : Exception @@ -39,9 +39,14 @@ public MissingKeyException(string message, Exception innerException) { } + /// + /// Initializes a new instance of the class. + /// + /// The serialization info. + /// The serialization context. protected MissingKeyException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) : base(serializationInfo, streamingContext) { } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Node.cs b/src/DynamicData/Cache/Node.cs index 32b8c39e4..1f6b51c34 100644 --- a/src/DynamicData/Cache/Node.cs +++ b/src/DynamicData/Cache/Node.cs @@ -1,26 +1,30 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Reactive.Disposables; -using DynamicData.Annotations; + using DynamicData.Kernel; + // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Node describing the relationship between and item and it's ancestors and descendent + /// Node describing the relationship between and item and it's ancestors and descendent. /// /// The type of the object. /// The type of the key. public class Node : IDisposable, IEquatable> + where TKey : notnull where TObject : class { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed with _cleanUp")] private readonly ISourceCache, TKey> _children = new SourceCache, TKey>(n => n.Key); + private readonly IDisposable _cleanUp; + private bool _isDisposed; /// @@ -29,7 +33,7 @@ public class Node : IDisposable, IEquatable> /// The item. /// The key. public Node(TObject item, TKey key) - : this(item, key, null) + : this(item, key, default) { } @@ -39,8 +43,7 @@ public Node(TObject item, TKey key) /// The item. /// The key. /// The parent. - /// - public Node([NotNull] TObject item, TKey key, Optional> parent) + public Node(TObject item, TKey key, Optional> parent) { Item = item ?? throw new ArgumentNullException(nameof(item)); Key = key; @@ -50,35 +53,12 @@ public Node([NotNull] TObject item, TKey key, Optional> pare } /// - /// The item - /// - public TObject Item { get; } - - /// - /// The key - /// - public TKey Key { get; } - - /// - /// Gets the parent if it has one - /// - public Optional> Parent { get; internal set; } - - /// - /// The child nodes + /// Gets the child nodes. /// public IObservableCache, TKey> Children { get; } /// - /// Gets or sets a value indicating whether this instance is root. - /// - /// - /// true if this instance is root node; otherwise, false. - /// - public bool IsRoot => !Parent.HasValue; - - /// - /// Gets the depth i.e. how many degrees of separation from the parent + /// Gets the depth i.e. how many degrees of separation from the parent. /// public int Depth { @@ -95,23 +75,71 @@ public int Depth i++; parent = parent.Value.Parent; - } while (true); + } + while (true); + return i; } } - internal void Update(Action, TKey>> updateAction) + /// + /// Gets a value indicating whether this instance is root. + /// + /// + /// true if this instance is root node; otherwise, false. + /// + public bool IsRoot => !Parent.HasValue; + + /// + /// Gets the item. + /// + public TObject Item { get; } + + /// + /// Gets the key. + /// + public TKey Key { get; } + + /// + /// Gets the parent if it has one. + /// + public Optional> Parent { get; internal set; } + + /// + /// Determines whether the specified objects are equal. + /// + /// The left value to compare. + /// The right value to compare. + /// If the two values are equal. + public static bool operator ==(Node? left, Node? right) { - _children.Edit(updateAction); + return Equals(left, right); } - #region Equality + /// + /// Determines whether the specified objects are not equal. + /// + /// The left value to compare. + /// The right value to compare. + /// If the two values are not equal. + public static bool operator !=(Node left, Node right) + { + return !Equals(left, right); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// 2. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } /// Determines whether the specified object is equal to the current object. /// true if the specified object is equal to the current object; otherwise, false. /// The object to compare with the current object. - /// 2 - public bool Equals(Node other) + /// 2. + public bool Equals(Node? other) { if (ReferenceEquals(null, other)) { @@ -129,8 +157,8 @@ public bool Equals(Node other) /// Determines whether the specified object is equal to the current object. /// true if the specified object is equal to the current object; otherwise, false. /// The object to compare with the current object. - /// 2 - public override bool Equals(object obj) + /// 2. + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -152,50 +180,33 @@ public override bool Equals(object obj) /// Serves as the default hash function. /// A hash code for the current object. - /// 2 + /// 2. public override int GetHashCode() { return EqualityComparer.Default.GetHashCode(Key); } /// - /// Determines whether the specified objects are equal - /// - public static bool operator ==(Node left, Node right) - { - return Equals(left, right); - } - - /// - /// Determines whether the specified objects are not equal - /// - public static bool operator !=(Node left, Node right) - { - return !Equals(left, right); - } - - #endregion - - /// - /// Returns a that represents this instance. + /// Returns a that represents this instance. /// /// - /// A that represents this instance. + /// A that represents this instance. /// public override string ToString() { - var count = Children.Count == 0 ? "" : $" ({Children.Count} children)"; + var count = Children.Count == 0 ? string.Empty : $" ({Children.Count} children)"; return $"{Item}{count}"; } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// 2 - public void Dispose() + internal void Update(Action, TKey>> updateAction) { - Dispose(true); - GC.SuppressFinalize(this); + _children.Edit(updateAction); } + /// + /// Disposes any managed or unmanaged resources. + /// + /// If the dispose is being called by the Dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -207,8 +218,8 @@ protected virtual void Dispose(bool isDisposing) if (isDisposing) { - _cleanUp?.Dispose(); + _cleanUp.Dispose(); } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ObservableCache.cs b/src/DynamicData/Cache/ObservableCache.cs index 4ce3becde..7905a5448 100644 --- a/src/DynamicData/Cache/ObservableCache.cs +++ b/src/DynamicData/Cache/ObservableCache.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,6 +7,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; + using DynamicData.Cache.Internal; using DynamicData.Kernel; @@ -14,13 +15,21 @@ namespace DynamicData { internal sealed class ObservableCache : IObservableCache + where TKey : notnull { - private readonly Subject> _changes = new Subject>(); - private readonly Subject> _changesPreview = new Subject>(); - private readonly Lazy> _countChanged = new Lazy>(() => new Subject()); - private readonly ReaderWriter _readerWriter; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed with _cleanUp")] + private readonly Subject> _changes = new(); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed with _cleanUp")] + private readonly Subject> _changesPreview = new(); + private readonly IDisposable _cleanUp; - private readonly object _locker = new object(); + + private readonly Lazy> _countChanged = new(() => new Subject()); + + private readonly object _locker = new(); + + private readonly ReaderWriter _readerWriter; private int _editLevel; // The level of recursion in editing. @@ -28,60 +37,137 @@ public ObservableCache(IObservable> source) { _readerWriter = new ReaderWriter(); - var loader = source.Synchronize(_locker) - .Finally(()=> - { - _changes.OnCompleted(); - _changesPreview.OnCompleted(); - }) - .Subscribe(changeset => - { - var previewHandler = _changesPreview.HasObservers ? (Action>)InvokePreview : null; - var changes = _readerWriter.Write(changeset, previewHandler, _changes.HasObservers); - InvokeNext(changes); - }, ex => - { - _changesPreview.OnError(ex); - _changes.OnError(ex); - }); + var loader = source.Synchronize(_locker).Finally( + () => + { + _changes.OnCompleted(); + _changesPreview.OnCompleted(); + }).Subscribe( + changeSet => + { + var previewHandler = _changesPreview.HasObservers ? (Action>)InvokePreview : null; + var changes = _readerWriter.Write(changeSet, previewHandler, _changes.HasObservers); + InvokeNext(changes); + }, + ex => + { + _changesPreview.OnError(ex); + _changes.OnError(ex); + }); - _cleanUp = Disposable.Create(() => - { - loader.Dispose(); - _changes.OnCompleted(); - _changesPreview.OnCompleted(); - if (_countChanged.IsValueCreated) - { - _countChanged.Value.OnCompleted(); - } - }); + _cleanUp = Disposable.Create( + () => + { + loader.Dispose(); + _changes.OnCompleted(); + _changesPreview.OnCompleted(); + if (_countChanged.IsValueCreated) + { + _countChanged.Value.OnCompleted(); + } + }); } - public ObservableCache(Func keySelector = null) + public ObservableCache(Func? keySelector = null) { _readerWriter = new ReaderWriter(keySelector); - _cleanUp = Disposable.Create(() => - { - _changes.OnCompleted(); - _changesPreview.OnCompleted(); - if (_countChanged.IsValueCreated) - { - _countChanged.Value.OnCompleted(); - } - }); + _cleanUp = Disposable.Create( + () => + { + _changes.OnCompleted(); + _changesPreview.OnCompleted(); + if (_countChanged.IsValueCreated) + { + _countChanged.Value.OnCompleted(); + } + }); + } + + public int Count => _readerWriter.Count; + + public IObservable CountChanged => + Observable.Create( + observer => + { + lock (_locker) + { + var source = _countChanged.Value.StartWith(_readerWriter.Count).DistinctUntilChanged(); + return source.SubscribeSafe(observer); + } + }); + + public IEnumerable Items => _readerWriter.Items; + + public IEnumerable Keys => _readerWriter.Keys; + + public IEnumerable> KeyValues => _readerWriter.KeyValues; + + public IObservable> Connect(Func? predicate = null) => + Observable.Create>( + observer => + { + lock (_locker) + { + var initial = GetInitialUpdates(predicate); + if (initial.Count != 0) + { + observer.OnNext(initial); + } + + var updateSource = (predicate is null ? _changes : _changes.Filter(predicate)).NotEmpty(); + return updateSource.SubscribeSafe(observer); + } + }); + + public void Dispose() => _cleanUp.Dispose(); + + public Optional Lookup(TKey key) => _readerWriter.Lookup(key); + + public IObservable> Preview(Func? predicate = null) + { + return predicate is null ? _changesPreview : _changesPreview.Filter(predicate); } + public IObservable> Watch(TKey key) => + Observable.Create>( + observer => + { + lock (_locker) + { + var initial = _readerWriter.Lookup(key); + if (initial.HasValue) + { + observer.OnNext(new Change(ChangeReason.Add, key, initial.Value)); + } + + return _changes.Finally(observer.OnCompleted).Subscribe( + changes => + { + foreach (var change in changes) + { + var match = EqualityComparer.Default.Equals(change.Key, key); + if (match) + { + observer.OnNext(change); + } + } + }); + } + }); + + internal ChangeSet GetInitialUpdates(Func? filter = null) => _readerWriter.GetInitialUpdates(filter); + internal void UpdateFromIntermediate(Action> updateAction) { - if (updateAction == null) + if (updateAction is null) { throw new ArgumentNullException(nameof(updateAction)); } lock (_locker) { - ChangeSet changes = null; + ChangeSet? changes = null; _editLevel++; if (_editLevel == 1) @@ -96,7 +182,7 @@ internal void UpdateFromIntermediate(Action> update _editLevel--; - if (_editLevel == 0) + if (changes is not null && _editLevel == 0) { InvokeNext(changes); } @@ -105,14 +191,14 @@ internal void UpdateFromIntermediate(Action> update internal void UpdateFromSource(Action> updateAction) { - if (updateAction == null) + if (updateAction is null) { throw new ArgumentNullException(nameof(updateAction)); } lock (_locker) { - ChangeSet changes = null; + ChangeSet? changes = null; _editLevel++; if (_editLevel == 1) @@ -127,24 +213,13 @@ internal void UpdateFromSource(Action> updateActio _editLevel--; - if (_editLevel == 0) + if (changes is not null && _editLevel == 0) { InvokeNext(changes); } } } - private void InvokePreview(ChangeSet changes) - { - lock (_locker) - { - if (changes.Count != 0) - { - _changesPreview.OnNext(changes); - } - } - } - private void InvokeNext(ChangeSet changes) { lock (_locker) @@ -161,71 +236,15 @@ private void InvokeNext(ChangeSet changes) } } - public IObservable CountChanged => Observable.Create(observer => - { - lock (_locker) - { - var source = _countChanged.Value.StartWith(_readerWriter.Count).DistinctUntilChanged(); - return source.SubscribeSafe(observer); - } - }); - - public IObservable> Watch(TKey key) => Observable.Create>(observer => - { - lock (_locker) - { - var initial = _readerWriter.Lookup(key); - if (initial.HasValue) - { - observer.OnNext(new Change(ChangeReason.Add, key, initial.Value)); - } - - return _changes.Finally(observer.OnCompleted).Subscribe(changes => - { - foreach (var change in changes) - { - var match = EqualityComparer.Default.Equals(change.Key, key); - if (match) - { - observer.OnNext(change); - } - } - }); - } - }); - - public IObservable> Connect(Func predicate = null) => Observable.Create>(observer => + private void InvokePreview(ChangeSet changes) { lock (_locker) { - var initial = GetInitialUpdates(predicate); - if (initial.Count != 0) + if (changes.Count != 0) { - observer.OnNext(initial); + _changesPreview.OnNext(changes); } - - var updateSource = (predicate == null ? _changes : _changes.Filter(predicate)).NotEmpty(); - return updateSource.SubscribeSafe(observer); } - }); - - public IObservable> Preview(Func predicate = null) - { - return predicate == null ? _changesPreview : _changesPreview.Filter(predicate); } - - internal ChangeSet GetInitialUpdates(Func filter = null) => _readerWriter.GetInitialUpdates(filter); - - public Optional Lookup(TKey key) => _readerWriter.Lookup(key); - - public IEnumerable Keys => _readerWriter.Keys; - - public IEnumerable> KeyValues => _readerWriter.KeyValues; - - public IEnumerable Items => _readerWriter.Items; - - public int Count => _readerWriter.Count; - - public void Dispose() => _cleanUp.Dispose(); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/ObservableCacheEx.cs b/src/DynamicData/Cache/ObservableCacheEx.cs index de8eac808..915b216c5 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reactive; @@ -14,7 +15,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; -using DynamicData.Annotations; + using DynamicData.Binding; using DynamicData.Cache.Internal; using DynamicData.Kernel; @@ -23,858 +24,836 @@ namespace DynamicData { /// - /// Extensions for dynamic data + /// Extensions for dynamic data. /// - [PublicAPI] public static class ObservableCacheEx { - #region General + private const int DefaultSortResetThreshold = 100; /// - /// Ensure that finally is always called. Thanks to Lee Campbell for this + /// Inject side effects into the stream using the specified adaptor. /// - /// + /// The type of the object. + /// The type of the key. /// The source. - /// The finally action. - /// - /// source - [Obsolete("This can cause unhandled exception issues so do not use")] - public static IObservable FinallySafe(this IObservable source, Action finallyAction) + /// The adaptor. + /// An observable which will emit change sets. + /// + /// source + /// or + /// destination. + /// + public static IObservable> Adapt(this IObservable> source, IChangeSetAdaptor adaptor) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (finallyAction == null) + if (adaptor is null) { - throw new ArgumentNullException(nameof(finallyAction)); + throw new ArgumentNullException(nameof(adaptor)); } - return new FinallySafe(source, finallyAction).Run(); + return source.Do(adaptor.Adapt); } /// - /// Cache equivalent to Publish().RefCount(). The source is cached so long as there is at least 1 subscriber. + /// Inject side effects into the stream using the specified sorted adaptor. /// /// The type of the object. - /// The type of the destination key. + /// The type of the key. /// The source. - /// - public static IObservable> RefCount(this IObservable> source) + /// The adaptor. + /// An observable which will emit change sets. + /// + /// source + /// or + /// destination. + /// + public static IObservable> Adapt(this IObservable> source, ISortedChangeSetAdaptor adaptor) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return new RefCount(source).Run(); - } + if (adaptor is null) + { + throw new ArgumentNullException(nameof(adaptor)); + } - /// - /// Monitors the status of a stream - /// - /// - /// The source. - /// - /// source - public static IObservable MonitorStatus(this IObservable source) - { - return new StatusMonitor(source).Run(); + return source.Do(adaptor.Adapt); } /// - /// Supresses updates which are empty + /// Adds or updates the cache with the specified item. /// /// The type of the object. /// The type of the key. /// The source. - /// - /// source - public static IObservable> NotEmpty(this IObservable> source) + /// The item. + /// source. + public static void AddOrUpdate(this ISourceCache source, TObject item) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Where(changes => changes.Count != 0); + source.Edit(updater => updater.AddOrUpdate(item)); } /// - /// Supresses updates which are empty. However it will produce a notification for the first change. + /// Adds or updates the cache with the specified item. /// /// The type of the object. /// The type of the key. /// The source. - /// - /// source - internal static IObservable> NotEmpty_Experiment(this IObservable> source) + /// The item. + /// The equality comparer used to determine whether a new item is the same as an existing cached item. + /// source. + public static void AddOrUpdate(this ISourceCache source, TObject item, IEqualityComparer equalityComparer) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Publish(shared => - { - return shared.Take(1) - .Merge(shared.Skip(1).Where(changes => changes.Count != 0)); - }); + source.Edit(updater => updater.AddOrUpdate(item, equalityComparer)); } /// - /// Flattens an update collection to it's individual items + /// + /// Adds or updates the cache with the specified items. + /// /// /// The type of the object. /// The type of the key. /// The source. - /// - /// source - public static IObservable> Flatten( - this IObservable> source) + /// The items. + /// source. + public static void AddOrUpdate(this ISourceCache source, IEnumerable items) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.SelectMany(changes => changes); + source.Edit(updater => updater.AddOrUpdate(items)); } /// - /// Provides a call back for each change + /// Removes the specified key from the cache. + /// If the item is not contained in the cache then the operation does nothing. /// /// The type of the object. /// The type of the key. - /// The source. - /// The action. - /// - /// - /// - public static IObservable> ForEachChange( - [NotNull] this IObservable> source, - [NotNull] Action> action) + /// The source cache. + /// The item to add or update. + /// The key to add or update. + /// source. + public static void AddOrUpdate(this IIntermediateCache source, TObject item, TKey key) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (action == null) + if (item is null) { - throw new ArgumentNullException(nameof(action)); + throw new ArgumentNullException(nameof(item)); } - return source.Do(changes => changes.ForEach(action)); - } - - /// - /// Ignores updates when the update is the same reference - /// - /// - /// - /// - /// - public static IObservable> IgnoreSameReferenceUpdate(this IObservable> source) - { - return source.IgnoreUpdateWhen((c, p) => ReferenceEquals(c,p)); + source.Edit(updater => updater.AddOrUpdate(item, key)); } /// - /// Ignores the update when the condition is met. - /// The first parameter in the ignore function is the current value and the second parameter is the previous value + /// Applied a logical And operator between the collections i.e items which are in all of the + /// sources are included. /// /// The type of the object. /// The type of the key. /// The source. - /// The ignore function (current,previous)=>{ return true to ignore }. - /// - public static IObservable> IgnoreUpdateWhen(this IObservable> source, - Func ignoreFunction) + /// The others. + /// An observable which emits change sets. + /// source or others. + public static IObservable> And(this IObservable> source, params IObservable>[] others) + where TKey : notnull { - return source.Select(updates => + if (source is null) { - var result = updates.Where(u => - { - if (u.Reason != ChangeReason.Update) - { - return true; - } + throw new ArgumentNullException(nameof(source)); + } + + if (others is null || others.Length == 0) + { + throw new ArgumentNullException(nameof(others)); + } - return !ignoreFunction(u.Current, u.Previous.Value); - }); - return new ChangeSet(result); - }).NotEmpty(); + return source.Combine(CombineOperator.And, others); } /// - /// Only includes the update when the condition is met. - /// The first parameter in the ignore function is the current value and the second parameter is the previous value + /// Applied a logical And operator between the collections i.e items which are in all of the sources are included. /// /// The type of the object. /// The type of the key. - /// The source. - /// The include function (current,previous)=>{ return true to include }. - /// - public static IObservable> IncludeUpdateWhen(this IObservable> source, - Func includeFunction) + /// The source. + /// An observable which emits change sets. + /// + /// source + /// or + /// others. + /// + public static IObservable> And(this ICollection>> sources) + where TKey : notnull { - if (source == null) + if (sources is null) { - throw new ArgumentNullException(nameof(source)); - } - - if (includeFunction == null) - { - throw new ArgumentNullException(nameof(includeFunction)); + throw new ArgumentNullException(nameof(sources)); } - return source.Select(changes => - { - var result = changes.Where(change => change.Reason != ChangeReason.Update || includeFunction(change.Current, change.Previous.Value)); - return new ChangeSet(result); - }).NotEmpty(); + return sources.Combine(CombineOperator.And); } /// - /// Dynamically merges the observable which is selected from each item in the stream, and unmerges the item - /// when it is no longer part of the stream. + /// Dynamically apply a logical And operator between the items in the outer observable list. + /// Items which are in all of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The type of the destination. - /// The source. - /// The observable selector. - /// - /// source - /// or - /// observableSelector - public static IObservable> MergeManyItems( - this IObservable> source, - Func> observableSelector) + /// The source. + /// An observable which emits change sets. + public static IObservable> And(this IObservableList>> sources) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (observableSelector == null) + if (sources is null) { - throw new ArgumentNullException(nameof(observableSelector)); + throw new ArgumentNullException(nameof(sources)); } - return new MergeManyItems(source, observableSelector).Run(); + return sources.Combine(CombineOperator.And); } /// - /// Dynamically merges the observable which is selected from each item in the stream, and unmerges the item - /// when it is no longer part of the stream. + /// Dynamically apply a logical And operator between the items in the outer observable list. + /// Items which are in all of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The type of the destination. - /// The source. - /// The observable selector. - /// - /// source - /// or - /// observableSelector - public static IObservable> MergeManyItems( - this IObservable> source, - Func> observableSelector) + /// The source. + /// An observable which emits change sets. + public static IObservable> And(this IObservableList> sources) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (observableSelector == null) + if (sources is null) { - throw new ArgumentNullException(nameof(observableSelector)); + throw new ArgumentNullException(nameof(sources)); } - return new MergeManyItems(source, observableSelector).Run(); + return sources.Combine(CombineOperator.And); } /// - /// Dynamically merges the observable which is selected from each item in the stream, and unmerges the item - /// when it is no longer part of the stream. + /// Dynamically apply a logical And operator between the items in the outer observable list. + /// Items which are in all of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The type of the destination. - /// The source. - /// The observable selector. - /// - /// source - /// or - /// observableSelector - public static IObservable MergeMany(this IObservable> source, Func> observableSelector) + /// The source. + /// An observable which emits change sets. + public static IObservable> And(this IObservableList> sources) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (observableSelector == null) + if (sources is null) { - throw new ArgumentNullException(nameof(observableSelector)); + throw new ArgumentNullException(nameof(sources)); } - return new MergeMany(source, observableSelector).Run(); + return sources.Combine(CombineOperator.And); } /// - /// Dynamically merges the observable which is selected from each item in the stream, and unmerges the item - /// when it is no longer part of the stream. + /// Converts the source to an read only observable cache. /// /// The type of the object. /// The type of the key. - /// The type of the destination. /// The source. - /// The observable selector. - /// - /// source - /// or - /// observableSelector - public static IObservable MergeMany(this IObservable> source, Func> observableSelector) + /// An observable cache. + /// source. + public static IObservableCache AsObservableCache(this IObservableCache source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (observableSelector == null) - { - throw new ArgumentNullException(nameof(observableSelector)); - } - - return new MergeMany(source, observableSelector).Run(); + return new AnonymousObservableCache(source); } /// - /// Watches each item in the collection and notifies when any of them has changed + /// Converts the source to a readonly observable cache. /// /// The type of the object. /// The type of the key. - /// The type of the value. /// The source. - /// The property accessor. - /// if set to true [notify on initial value]. - /// - /// - /// - public static IObservable WhenValueChanged([NotNull] this IObservable> source, - [NotNull] Expression> propertyAccessor, - bool notifyOnInitialValue = true) - where TObject : INotifyPropertyChanged + /// if set to true all methods are synchronised. There is no need to apply locking when the consumer can be sure the read / write operations are already synchronised. + /// An observable cache. + /// source. + public static IObservableCache AsObservableCache(this IObservable> source, bool applyLocking = true) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertyAccessor == null) + if (applyLocking) { - throw new ArgumentNullException(nameof(propertyAccessor)); + return new AnonymousObservableCache(source); } - return source.MergeMany(t => t.WhenChanged(propertyAccessor, notifyOnInitialValue)); + return new LockFreeObservableCache(source); } /// - /// Watches each item in the collection and notifies when any of them has changed + /// Automatically refresh downstream operators when any properties change. /// - /// The type of the object. - /// The type of the key. - /// The type of the value. - /// The source. - /// The property accessor. - /// if set to true [notify on initial value]. - /// - /// - /// - public static IObservable> WhenPropertyChanged([NotNull] this IObservable> source, - [NotNull] Expression> propertyAccessor, - bool notifyOnInitialValue = true) + /// The object of the change set. + /// The key of the change set. + /// The source observable. + /// Batch up changes by specifying the buffer. This greatly increases performance when many elements have successive property changes. + /// When observing on multiple property changes, apply a throttle to prevent excessive refresh invocations. + /// The scheduler. + /// An observable change set with additional refresh changes. + public static IObservable> AutoRefresh(this IObservable> source, TimeSpan? changeSetBuffer = null, TimeSpan? propertyChangeThrottle = null, IScheduler? scheduler = null) + where TKey : notnull where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertyAccessor == null) - { - throw new ArgumentNullException(nameof(propertyAccessor)); - } + return source.AutoRefreshOnObservable( + (t, _) => + { + if (propertyChangeThrottle is null) + { + return t.WhenAnyPropertyChanged(); + } - return source.MergeMany(t => t.WhenPropertyChanged(propertyAccessor, notifyOnInitialValue)); + return t.WhenAnyPropertyChanged().Throttle(propertyChangeThrottle.Value, scheduler ?? Scheduler.Default); + }, + changeSetBuffer, + scheduler); } /// - /// Watches each item in the collection and notifies when any of them has changed + /// Automatically refresh downstream operators when properties change. /// - /// The type of the object. - /// The type of the key. - /// specify properties to Monitor, or omit to monitor all property changes - /// The source. - /// - /// - /// - public static IObservable WhenAnyPropertyChanged([NotNull] this IObservable> source, params string[] propertiesToMonitor) + /// The object of the change set. + /// The key of the change set. + /// The type of the property. + /// The source observable. + /// Specify a property to observe changes. When it changes a Refresh is invoked. + /// Batch up changes by specifying the buffer. This greatly increases performance when many elements have successive property changes. + /// When observing on multiple property changes, apply a throttle to prevent excessive refresh invocations. + /// The scheduler. + /// An observable change set with additional refresh changes. + public static IObservable> AutoRefresh(this IObservable> source, Expression> propertyAccessor, TimeSpan? changeSetBuffer = null, TimeSpan? propertyChangeThrottle = null, IScheduler? scheduler = null) + where TKey : notnull where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.MergeMany(t => t.WhenAnyPropertyChanged(propertiesToMonitor)); + return source.AutoRefreshOnObservable( + (t, _) => + { + if (propertyChangeThrottle is null) + { + return t.WhenPropertyChanged(propertyAccessor, false); + } + + return t.WhenPropertyChanged(propertyAccessor, false).Throttle(propertyChangeThrottle.Value, scheduler ?? Scheduler.Default); + }, + changeSetBuffer, + scheduler); } /// - /// Subscribes to each item when it is added to the stream and unsubcribes when it is removed. All items will be unsubscribed when the stream is disposed + /// Automatically refresh downstream operator. The refresh is triggered when the observable receives a notification. /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The subsription function - /// - /// source - /// or - /// subscriptionFactory - /// - /// Subscribes to each item when it is added or updates and unsubcribes when it is removed - /// - public static IObservable> SubscribeMany(this IObservable> source, - Func subscriptionFactory) + /// The object of the change set. + /// The key of the change set. + /// The type of evaluation. + /// The source observable change set. + /// An observable which acts on items within the collection and produces a value when the item should be refreshed. + /// Batch up changes by specifying the buffer. This greatly increases performance when many elements require a refresh. + /// The scheduler. + /// An observable change set with additional refresh changes. + public static IObservable> AutoRefreshOnObservable(this IObservable> source, Func> reevaluator, TimeSpan? changeSetBuffer = null, IScheduler? scheduler = null) + where TKey : notnull + { + return source.AutoRefreshOnObservable((t, _) => reevaluator(t), changeSetBuffer, scheduler); + } + + /// + /// Automatically refresh downstream operator. The refresh is triggered when the observable receives a notification. + /// + /// The object of the change set. + /// The key of the change set. + /// The type of evaluation. + /// The source observable change set. + /// An observable which acts on items within the collection and produces a value when the item should be refreshed. + /// Batch up changes by specifying the buffer. This greatly increases performance when many elements require a refresh. + /// The scheduler. + /// An observable change set with additional refresh changes. + public static IObservable> AutoRefreshOnObservable(this IObservable> source, Func> reevaluator, TimeSpan? changeSetBuffer = null, IScheduler? scheduler = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (subscriptionFactory == null) + if (reevaluator is null) { - throw new ArgumentNullException(nameof(subscriptionFactory)); + throw new ArgumentNullException(nameof(reevaluator)); } - return new SubscribeMany(source, subscriptionFactory).Run(); + return new AutoRefresh(source, reevaluator, changeSetBuffer, scheduler).Run(); } /// - /// Subscribes to each item when it is added to the stream and unsubcribes when it is removed. All items will be unsubscribed when the stream is disposed + /// Batches the updates for the specified time period. /// /// The type of the object. /// The type of the key. /// The source. - /// The subsription function - /// + /// The time span. + /// The scheduler. + /// An observable which emits change sets. /// source /// or - /// subscriptionFactory - /// - /// Subscribes to each item when it is added or updates and unsubcribes when it is removed - /// - public static IObservable> SubscribeMany(this IObservable> source, - Func subscriptionFactory) + /// scheduler. + public static IObservable> Batch(this IObservable> source, TimeSpan timeSpan, IScheduler? scheduler = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (subscriptionFactory == null) - { - throw new ArgumentNullException(nameof(subscriptionFactory)); - } - - return new SubscribeMany(source, subscriptionFactory).Run(); + return source.Buffer(timeSpan, scheduler ?? Scheduler.Default).FlattenBufferResult(); } /// - /// Callback for each item as and when it is being added to the stream + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. /// /// The type of the object. /// The type of the key. /// The source. - /// The add action. - /// - public static IObservable> OnItemAdded(this IObservable> source, [NotNull] Action addAction) + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// The scheduler. + /// An observable which emits change sets. + /// source. + public static IObservable> BatchIf(this IObservable> source, IObservable pauseIfTrueSelector, IScheduler? scheduler = null) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (addAction == null) - { - throw new ArgumentNullException(nameof(addAction)); - } - - return source.Do(changes => changes.Where(c => c.Reason == ChangeReason.Add) - .ForEach(c => addAction(c.Current))); + return BatchIf(source, pauseIfTrueSelector, false, scheduler); } /// - /// Callback for each item as and when it is being removed from the stream + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. /// /// The type of the object. /// The type of the key. /// The source. - /// The remove action. - /// - /// - /// source - /// or - /// removeAction - /// - public static IObservable> OnItemRemoved(this IObservable> source, Action removeAction) + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// if set to true [initial pause state]. + /// The scheduler. + /// An observable which emits change sets. + /// source. + public static IObservable> BatchIf(this IObservable> source, IObservable pauseIfTrueSelector, bool initialPauseState = false, IScheduler? scheduler = null) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (removeAction == null) - { - throw new ArgumentNullException(nameof(removeAction)); - } + return new BatchIf(source, pauseIfTrueSelector, null, initialPauseState, scheduler: scheduler).Run(); + } - return source.Do(changes => changes.Where(c => c.Reason == ChangeReason.Remove) - .ForEach(c => removeAction(c.Current))); + /// + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// Specify a time to ensure the buffer window does not stay open for too long. On completion buffering will cease. + /// The scheduler. + /// An observable which emits change sets. + /// source. + public static IObservable> BatchIf(this IObservable> source, IObservable pauseIfTrueSelector, TimeSpan? timeOut = null, IScheduler? scheduler = null) + where TKey : notnull + { + return BatchIf(source, pauseIfTrueSelector, false, timeOut, scheduler); } /// - /// Callback when an item has been updated eg. (current, previous)=>{} + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. /// /// The type of the object. /// The type of the key. /// The source. - /// The update action. - /// - /// - /// - public static IObservable> OnItemUpdated(this IObservable> source, Action updateAction) + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// if set to true [initial pause state]. + /// Specify a time to ensure the buffer window does not stay open for too long. On completion buffering will cease. + /// The scheduler. + /// An observable which emits change sets. + /// source. + public static IObservable> BatchIf(this IObservable> source, IObservable pauseIfTrueSelector, bool initialPauseState = false, TimeSpan? timeOut = null, IScheduler? scheduler = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (updateAction == null) + if (pauseIfTrueSelector is null) { - throw new ArgumentNullException(nameof(updateAction)); + throw new ArgumentNullException(nameof(pauseIfTrueSelector)); } - return source.Do(changes => changes.Where(c => c.Reason == ChangeReason.Update) - .ForEach(c => updateAction(c.Current, c.Previous.Value))); + return new BatchIf(source, pauseIfTrueSelector, timeOut, initialPauseState, scheduler: scheduler).Run(); } /// - /// Disposes each item when no longer required. - /// - /// Individual items are disposed when removed or replaced. All items - /// are disposed when the stream is disposed + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. /// - /// - /// /// The type of the object. /// The type of the key. /// The source. - /// A continuation of the original stream - /// source - public static IObservable> DisposeMany(this IObservable> source) + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// if set to true [initial pause state]. + /// Specify a time observable. The buffer will be emptied each time the timer produces a value and when it completes. On completion buffering will cease. + /// The scheduler. + /// An observable which emits change sets. + /// source. + public static IObservable> BatchIf(this IObservable> source, IObservable pauseIfTrueSelector, bool initialPauseState = false, IObservable? timer = null, IScheduler? scheduler = null) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return new DisposeMany(source, t => - { - var d = t as IDisposable; - d?.Dispose(); - }) - .Run(); + return new BatchIf(source, pauseIfTrueSelector, null, initialPauseState, timer, scheduler).Run(); } /// - /// Includes changes for the specified reasons only + /// 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 reasons. - /// - /// reasons - /// Must select at least on reason - public static IObservable> WhereReasonsAre( - this IObservable> source, params ChangeReason[] reasons) + /// The destination. + /// An observable which will emit change sets. + /// source. + public static IObservable> Bind(this IObservable> source, IObservableCollection destination) + where TKey : notnull { - if (reasons == null) + if (source is null) { - throw new ArgumentNullException(nameof(reasons)); + throw new ArgumentNullException(nameof(source)); } - if (!reasons.Any()) + if (destination is null) { - throw new ArgumentException("Must select at least one reason"); + throw new ArgumentNullException(nameof(destination)); } - var hashed = new HashSet(reasons); - - return source.Select(updates => - { - return new ChangeSet(updates.Where(u => hashed.Contains(u.Reason))); - }).NotEmpty(); + var updater = new ObservableCollectionAdaptor(); + return source.Bind(destination, updater); } /// - /// Excludes updates for the specified reasons + /// 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 reasons. - /// - /// reasons - /// Must select at least on reason - public static IObservable> WhereReasonsAreNot(this IObservable> source, params ChangeReason[] reasons) + /// The destination. + /// The updater. + /// An observable which will emit change sets. + /// source. + public static IObservable> Bind(this IObservable> source, IObservableCollection destination, IObservableCollectionAdaptor updater) + where TKey : notnull { - if (reasons == null) + if (source is null) { - throw new ArgumentNullException(nameof(reasons)); + throw new ArgumentNullException(nameof(source)); } - if (!reasons.Any()) + if (destination is null) { - throw new ArgumentException("Must select at least one reason"); + throw new ArgumentNullException(nameof(destination)); } - var hashed = new HashSet(reasons); - - return source.Select(updates => + if (updater is null) { - return new ChangeSet(updates.Where(u => !hashed.Contains(u.Reason))); - }).NotEmpty(); - } - - #endregion + throw new ArgumentNullException(nameof(updater)); + } - #region Auto Refresh + return Observable.Create>( + observer => + { + var locker = new object(); + return source.Synchronize(locker).Select( + changes => + { + updater.Adapt(changes, destination); + return changes; + }).SubscribeSafe(observer); + }); + } /// - /// Automatically refresh downstream operators when any properties change. + /// Binds the results to the specified observable collection using the default update algorithm. /// - /// The source observable - /// Batch up changes by specifying the buffer. This greatly increases performance when many elements have sucessive property changes - /// When observing on multiple property changes, apply a throttle to prevent excessive refesh invocations - /// The scheduler - /// An observable change set with additional refresh changes - public static IObservable> AutoRefresh(this IObservable> source, - TimeSpan? changeSetBuffer = null, - TimeSpan? propertyChangeThrottle = null, - IScheduler scheduler = null) - where TObject : INotifyPropertyChanged + /// 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 TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.AutoRefreshOnObservable((t, v) => + if (destination is null) { - if (propertyChangeThrottle == null) - { - return t.WhenAnyPropertyChanged(); - } + throw new ArgumentNullException(nameof(destination)); + } - return t.WhenAnyPropertyChanged() - .Throttle(propertyChangeThrottle.Value, scheduler ?? Scheduler.Default); - }, changeSetBuffer, scheduler); + var updater = new SortedObservableCollectionAdaptor(); + return source.Bind(destination, updater); } /// - /// Automatically refresh downstream operators when properties change. + /// Binds the results to the specified binding collection using the specified update algorithm. /// - /// The source observable - /// Specify a property to observe changes. When it changes a Refresh is invoked - /// Batch up changes by specifying the buffer. This greatly increases performance when many elements have sucessive property changes - /// When observing on multiple property changes, apply a throttle to prevent excessive refesh invocations - /// The scheduler - /// An observable change set with additional refresh changes - public static IObservable> AutoRefresh(this IObservable> source, - Expression> propertyAccessor, - TimeSpan? changeSetBuffer = null, - TimeSpan? propertyChangeThrottle = null, - IScheduler scheduler = null) - where TObject : INotifyPropertyChanged + /// 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 TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.AutoRefreshOnObservable((t, v) => + if (destination is null) { - if (propertyChangeThrottle == null) - { - return t.WhenPropertyChanged(propertyAccessor, false); - } + throw new ArgumentNullException(nameof(destination)); + } - return t.WhenPropertyChanged(propertyAccessor, false) - .Throttle(propertyChangeThrottle.Value, scheduler ?? Scheduler.Default); - }, changeSetBuffer, scheduler); - } + if (updater is null) + { + throw new ArgumentNullException(nameof(updater)); + } - /// - /// Automatically refresh downstream operator. The refresh is triggered when the observable receives a notification - /// - /// The source observable change set - /// An observable which acts on items within the collection and produces a value when the item should be refreshed - /// Batch up changes by specifying the buffer. This greatly increases performance when many elements require a refresh - /// The scheduler - /// An observable change set with additional refresh changes - public static IObservable> AutoRefreshOnObservable(this IObservable> source, - Func> reevaluator, - TimeSpan? changeSetBuffer = null, - IScheduler scheduler = null) - { - return source.AutoRefreshOnObservable((t, v) => reevaluator(t), changeSetBuffer, scheduler); + return Observable.Create>( + observer => + { + var locker = new object(); + return source.Synchronize(locker).Select( + changes => + { + updater.Adapt(changes, destination); + return changes; + }).SubscribeSafe(observer); + }); } /// - /// Automatically refresh downstream operator. The refresh is triggered when the observable receives a notification + /// Binds the results to the specified readonly observable collection using the default update algorithm. /// - /// The source observable change set - /// An observable which acts on items within the collection and produces a value when the item should be refreshed - /// Batch up changes by specifying the buffer. This g reatly increases performance when many elements require a refresh - /// The scheduler - /// An observable change set with additional refresh changes - public static IObservable> AutoRefreshOnObservable(this IObservable> source, - Func> reevaluator, - TimeSpan? changeSetBuffer = null, - IScheduler scheduler = null) + /// The type of the object. + /// 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. + /// 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, ISortedObservableCollectionAdaptor? adaptor = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (reevaluator == null) - { - throw new ArgumentNullException(nameof(reevaluator)); - } - - return new AutoRefresh(source, reevaluator, changeSetBuffer, scheduler).Run(); + var target = new ObservableCollectionExtended(); + var result = new ReadOnlyObservableCollection(target); + var updater = adaptor ?? new SortedObservableCollectionAdaptor(resetThreshold); + readOnlyObservableCollection = result; + return source.Bind(target, updater); } /// - /// Supress refresh notifications + /// Binds the results to the specified readonly observable collection using the default update algorithm. /// - /// The source observable change set - /// - public static IObservable> SupressRefresh(this IObservable> source) + /// The type of the object. + /// 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. + /// 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, IObservableCollectionAdaptor? adaptor = null) + where TKey : notnull { - return source.WhereReasonsAreNot(ChangeReason.Refresh); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - #endregion + var target = new ObservableCollectionExtended(); + var result = new ReadOnlyObservableCollection(target); + var updater = adaptor ?? new ObservableCollectionAdaptor(resetThreshold); + readOnlyObservableCollection = result; + return source.Bind(target, updater); + } - #region Start with +#if SUPPORTS_BINDINGLIST /// - /// Prepends an empty changeset to the source + /// Binds a clone of the observable change set to the target observable collection. /// - public static IObservable> StartWithEmpty(this IObservable> source) + /// The object type. + /// The key type. + /// The source. + /// The target binding list. + /// The reset threshold. + /// An observable which will emit change sets. + /// + /// source + /// or + /// targetCollection. + /// + public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = 25) + where TKey : notnull { - return source.StartWith(ChangeSet.Empty); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - /// - /// Prepends an empty changeset to the source - /// - /// - public static IObservable> StartWithEmpty(this IObservable> source) - { - return source.StartWith(SortedChangeSet.Empty); - } + if (bindingList is null) + { + throw new ArgumentNullException(nameof(bindingList)); + } - /// - /// Prepends an empty changeset to the source - /// - /// - public static IObservable> StartWithEmpty(this IObservable> source) - { - return source.StartWith(VirtualChangeSet.Empty); + return source.Adapt(new BindingListAdaptor(bindingList, resetThreshold)); } - /// - /// Prepends an empty changeset to the source - /// - /// - public static IObservable> StartWithEmpty(this IObservable> source) - { - return source.StartWith(PagedChangeSet.Empty); - } +#endif - /// - /// Prepends an empty changeset to the source - /// - /// - public static IObservable> StartWithEmpty(this IObservable> source) - { - return source.StartWith(GroupChangeSet.Empty); - } +#if SUPPORTS_BINDINGLIST /// - /// Prepends an empty changeset to the source + /// Binds a clone of the observable change set to the target observable collection. /// - /// - public static IObservable> StartWithEmpty(this IObservable> source) + /// The object type. + /// The key type. + /// The source. + /// The target binding list. + /// The reset threshold. + /// An observable which will emit change sets. + /// + /// source + /// or + /// targetCollection. + /// + public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = 25) + where TKey : notnull { - return source.StartWith(ImmutableGroupChangeSet.Empty); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (bindingList is null) + { + throw new ArgumentNullException(nameof(bindingList)); + } + + return source.Adapt(new SortedBindingListAdaptor(bindingList, resetThreshold)); } +#endif + /// - /// Prepends an empty changeset to the source + /// Buffers changes for an initial period only. After the period has elapsed, not further buffering occurs. /// - public static IObservable> StartWithEmpty(this IObservable> source) + /// The object type. + /// The type of the key. + /// The source change set. + /// The period to buffer, measure from the time that the first item arrives. + /// The scheduler to buffer on. + /// An observable which emits change sets. + public static IObservable> BufferInitial(this IObservable> source, TimeSpan initialBuffer, IScheduler? scheduler = null) + where TKey : notnull { - return source.StartWith(ReadOnlyCollectionLight.Empty); - } - - #endregion + return source.DeferUntilLoaded().Publish( + shared => + { + var initial = shared.Buffer(initialBuffer, scheduler ?? Scheduler.Default).FlattenBufferResult().Take(1); - #region Conversion + return initial.Concat(shared); + }); + } /// - /// Removes the key which enables all observable list features of dynamic data + /// Cast the object to the specified type. + /// Alas, I had to add the converter due to type inference issues. /// - /// - /// All indexed changes are dropped i.e. sorting is not supported by this function - /// - /// The type of object. - /// The type of key. + /// The type of the object. + /// The type of the key. + /// The type of the destination. /// The source. - /// - /// - /// - public static IObservable> RemoveKey([NotNull] this IObservable> source) + /// The conversion factory. + /// An observable which emits change sets. + public static IObservable> Cast(this IObservable> source, Func converter) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Select(changes => - { - var enumerator = new RemoveKeyEnumerator(changes); - return new ChangeSet(enumerator); - }); + return new Cast(source, converter).Run(); } /// @@ -884,26 +863,28 @@ public static IObservable> RemoveKey([NotNull /// The type of the source key. /// The type of the destination key. /// The source. - /// The key selector eg. (item) => newKey; - /// - /// source + /// The key selector eg. (item) => newKey. + /// An observable which emits change sets. public static IObservable> ChangeKey(this IObservable> source, Func keySelector) + where TSourceKey : notnull + where TDestinationKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return source.Select(updates => - { - var changed = updates.Select(u => new Change(u.Reason, keySelector(u.Current), u.Current, u.Previous)); - return new ChangeSet(changed); - }); + return source.Select( + updates => + { + var changed = updates.Select(u => new Change(u.Reason, keySelector(u.Current), u.Current, u.Previous)); + return new ChangeSet(changed); + }); } /// @@ -913,782 +894,592 @@ public static IObservable> ChangeKeyThe type of the source key. /// The type of the destination key. /// The source. - /// The key selector eg. (key, item) => newKey; - /// - /// source + /// The key selector eg. (key, item) => newKey. + /// An observable which emits change sets. + /// source. public static IObservable> ChangeKey(this IObservable> source, Func keySelector) + where TSourceKey : notnull + where TDestinationKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return source.Select(updates => - { - var changed = updates.Select(u => new Change(u.Reason, keySelector(u.Key, u.Current), u.Current, u.Previous)); - return new ChangeSet(changed); - }); + return source.Select( + updates => + { + var changed = updates.Select(u => new Change(u.Reason, keySelector(u.Key, u.Current), u.Current, u.Previous)); + return new ChangeSet(changed); + }); } /// - /// Convert the object using the sepcified conversion function. - /// This is a lighter equivalent of Transform and is designed to be used with non-disposable objects + /// Clears all data. /// /// The type of the object. /// The type of the key. - /// The type of the destination. /// The source. - /// The conversion factory. - /// - /// - [Obsolete("This was an experiment that did not work. Use Transform instead")] - public static IObservable> Convert(this IObservable> source, - Func conversionFactory) + /// source. + public static void Clear(this ISourceCache source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (conversionFactory == null) - { - throw new ArgumentNullException(nameof(conversionFactory)); - } - - return source.Select(changes => - { - var transformed = changes.Select(change => new Change(change.Reason, - change.Key, - conversionFactory(change.Current), - change.Previous.Convert(conversionFactory), - change.CurrentIndex, - change.PreviousIndex)); - return new ChangeSet(transformed); - }); + source.Edit(updater => updater.Clear()); } /// - /// Cast the object to the specified type. - /// Alas, I had to add the converter due to type inference issues + /// Clears all items from the cache. /// - /// The type of the object. + /// The type of the object. /// The type of the key. - /// The type of the destination. - /// The conversion factory. /// The source. - /// - /// - public static IObservable> Cast(this IObservable> source, Func converter) - + /// source. + public static void Clear(this IIntermediateCache source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return new Cast(source, converter).Run(); + source.Edit(updater => updater.Clear()); } - #endregion - - #region Delayed Stream - /// - /// Buffers changes for an intial period only. After the period has elapsed, not further buffering occurs. + /// Clears all data. /// - /// The source changeset - /// The period to buffer, measure from the time that the first item arrives - /// The scheduler to buffer on - public static IObservable> BufferInitial(this IObservable> source, TimeSpan initalBuffer, IScheduler scheduler = null) + /// The type of the object. + /// The type of the key. + /// The source. + /// source. + public static void Clear(this LockFreeObservableCache source) + where TKey : notnull { - return source.DeferUntilLoaded().Publish(shared => + if (source is null) { - var initial = shared.Buffer(initalBuffer, scheduler ?? Scheduler.Default) - .FlattenBufferResult() - .Take(1); + throw new ArgumentNullException(nameof(source)); + } - return initial.Concat(shared); - }); + source.Edit(updater => updater.Clear()); } /// - /// Batches the updates for the specified time period + /// Clones the changes into the specified collection. /// /// The type of the object. /// The type of the key. /// The source. - /// The time span. - /// The scheduler. - /// - /// source - /// or - /// scheduler - public static IObservable> Batch(this IObservable> source, - TimeSpan timeSpan, - IScheduler scheduler = null) + /// The target. + /// An observable which emits change sets. + public static IObservable> Clone(this IObservable> source, ICollection target) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Buffer(timeSpan, scheduler ?? Scheduler.Default).FlattenBufferResult(); + if (target is null) + { + throw new ArgumentNullException(nameof(target)); + } + + return source.Do( + changes => + { + foreach (var item in changes) + { + switch (item.Reason) + { + case ChangeReason.Add: + { + target.Add(item.Current); + } + + break; + + case ChangeReason.Update: + { + target.Remove(item.Previous.Value); + target.Add(item.Current); + } + + break; + + case ChangeReason.Remove: + target.Remove(item.Current); + break; + } + } + }); } /// - /// Convert the result of a buffer operation to a single change set + /// Convert the object using the specified conversion function. + /// This is a lighter equivalent of Transform and is designed to be used with non-disposable objects. /// /// The type of the object. /// The type of the key. + /// The type of the destination. /// The source. - /// - public static IObservable> FlattenBufferResult([NotNull] this IObservable>> source) + /// The conversion factory. + /// An observable which emits change sets. + [Obsolete("This was an experiment that did not work. Use Transform instead")] + public static IObservable> Convert(this IObservable> source, Func conversionFactory) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Where(x => x.Count != 0) - .Select(updates => new ChangeSet(updates.SelectMany(u => u))); + if (conversionFactory is null) + { + throw new ArgumentNullException(nameof(conversionFactory)); + } + + return source.Select( + changes => + { + var transformed = changes.Select(change => new Change(change.Reason, change.Key, conversionFactory(change.Current), change.Previous.Convert(conversionFactory), change.CurrentIndex, change.PreviousIndex)); + return new ChangeSet(transformed); + }); } /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. + /// Defer the subscription until the stream has been inflated with data. /// /// The type of the object. /// The type of the key. /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// The scheduler. - /// - /// source - public static IObservable> BatchIf(this IObservable> source, - IObservable pauseIfTrueSelector, - IScheduler scheduler = null) + /// An observable which emits change sets. + public static IObservable> DeferUntilLoaded(this IObservable> source) + where TKey : notnull { - return BatchIf(source, pauseIfTrueSelector, false, scheduler); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return new DeferUntilLoaded(source).Run(); } /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. + /// Defer the subscription until the stream has been inflated with data. /// /// The type of the object. /// The type of the key. /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// if set to true [intial pause state]. - /// The scheduler. - /// - /// source - public static IObservable> BatchIf(this IObservable> source, - IObservable pauseIfTrueSelector, - bool intialPauseState = false, - IScheduler scheduler = null) + /// An observable which emits change sets. + public static IObservable> DeferUntilLoaded(this IObservableCache source) + where TKey : notnull { - return new BatchIf(source, pauseIfTrueSelector, null, intialPauseState, scheduler: scheduler).Run(); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return new DeferUntilLoaded(source).Run(); } /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. + /// Disposes each item when no longer required. + /// + /// Individual items are disposed when removed or replaced. All items + /// are disposed when the stream is disposed. /// /// The type of the object. /// The type of the key. /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// Specify a time to ensure the buffer window does not stay open for too long. On completion buffering will cease - /// The scheduler. - /// - /// source - public static IObservable> BatchIf(this IObservable> source, - IObservable pauseIfTrueSelector, - TimeSpan? timeOut = null, - IScheduler scheduler = null) + /// A continuation of the original stream. + /// source. + public static IObservable> DisposeMany(this IObservable> source) + where TKey : notnull { - return BatchIf(source, pauseIfTrueSelector, false, timeOut, scheduler); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return new DisposeMany( + source, + t => + { + var d = t as IDisposable; + d?.Dispose(); + }).Run(); } /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. + /// Selects distinct values from the source. /// - /// The type of the object. + /// The type object from which the distinct values are selected. /// The type of the key. + /// The type of the value. /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// if set to true [intial pause state]. - /// Specify a time to ensure the buffer window does not stay open for too long. On completion buffering will cease - /// The scheduler. - /// - /// source - public static IObservable> BatchIf(this IObservable> source, - IObservable pauseIfTrueSelector, - bool intialPauseState = false, - TimeSpan? timeOut = null, - IScheduler scheduler = null) + /// The value selector. + /// An observable which will emit distinct change sets. + /// + /// Due to it's nature only adds or removes can be returned. + /// + /// source. + public static IObservable> DistinctValues(this IObservable> source, Func valueSelector) + where TKey : notnull + where TValue : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (pauseIfTrueSelector == null) + if (valueSelector is null) { - throw new ArgumentNullException(nameof(pauseIfTrueSelector)); + throw new ArgumentNullException(nameof(valueSelector)); } - return new BatchIf(source, pauseIfTrueSelector, timeOut, intialPauseState,scheduler: scheduler).Run(); - } - - /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// if set to true [intial pause state]. - /// Specify a time observable. The buffer will be emptied each time the timer produces a value and when it completes. On completion buffering will cease - /// The scheduler. - /// - /// source - public static IObservable> BatchIf(this IObservable> source, - IObservable pauseIfTrueSelector, - bool intialPauseState = false, - IObservable timer = null, - IScheduler scheduler = null) - { - return new BatchIf(source, pauseIfTrueSelector, null, intialPauseState, timer, scheduler: scheduler).Run(); + return Observable.Create>(observer => new DistinctCalculator(source, valueSelector).Run().SubscribeSafe(observer)); } /// - /// Defer the subscribtion until loaded and skip initial changeset + /// Loads the cache with the specified items in an optimised manner i.e. calculates the differences between the old and new items + /// in the list and amends only the differences. /// /// The type of the object. /// The type of the key. /// The source. - /// - /// source - public static IObservable> SkipInitial(this IObservable> source) + /// The items to add, update or delete. + /// The equality comparer used to determine whether a new item is the same as an existing cached item. + /// source. + public static void EditDiff(this ISourceCache source, IEnumerable allItems, IEqualityComparer equalityComparer) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.DeferUntilLoaded().Skip(1); - } + if (allItems is null) + { + throw new ArgumentNullException(nameof(allItems)); + } - /// - /// Defer the subscription until the stream has been inflated with data - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> DeferUntilLoaded(this IObservable> source) - { - if (source == null) + if (equalityComparer is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(equalityComparer)); } - return new DeferUntilLoaded(source).Run(); + source.EditDiff(allItems, equalityComparer.Equals); } /// - /// Defer the subscription until the stream has been inflated with data + /// Loads the cache with the specified items in an optimised manner i.e. calculates the differences between the old and new items + /// in the list and amends only the differences. /// /// The type of the object. /// The type of the key. /// The source. - /// - public static IObservable> DeferUntilLoaded(this IObservableCache source) + /// The items to compare and add, update or delete. + /// Expression to determine whether an item's value is equal to the old value (current, previous) => current.Version == previous.Version. + /// source. + public static void EditDiff(this ISourceCache source, IEnumerable allItems, Func areItemsEqual) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return new DeferUntilLoaded(source).Run(); - } + if (allItems is null) + { + throw new ArgumentNullException(nameof(allItems)); + } - #endregion + if (areItemsEqual is null) + { + throw new ArgumentNullException(nameof(areItemsEqual)); + } - #region True for all values + var editDiff = new EditDiff(source, areItemsEqual); + editDiff.Edit(allItems); + } /// - /// Produces a boolean observable indicating whether the latest resulting value from all of the specified observables matches - /// the equality condition. The observable is re-evaluated whenever - /// - /// i) The cache changes - /// or ii) The inner observable changes + /// Signal observers to re-evaluate the specified item. /// /// The type of the object. /// The type of the key. - /// The type of the value. /// The source. - /// Selector which returns the target observable - /// The equality condition. - /// - /// source - public static IObservable TrueForAll(this IObservable> source, - Func> observableSelector, - Func equalityCondition) + /// The item. + /// source. + [Obsolete(Constants.EvaluateIsDead)] + public static void Evaluate(this ISourceCache source, TObject item) + where TKey : notnull { - return source.TrueFor(observableSelector, - items => items.All(o => o.LatestValue.HasValue && equalityCondition(o.LatestValue.Value))); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + source.Edit(updater => updater.Refresh(item)); } /// - /// Produces a boolean observable indicating whether the latest resulting value from all of the specified observables matches - /// the equality condition. The observable is re-evaluated whenever - /// - /// i) The cache changes - /// or ii) The inner observable changes + /// Signal observers to re-evaluate the specified items. /// /// The type of the object. /// The type of the key. - /// The type of the value. /// The source. - /// Selector which returns the target observable - /// The equality condition. - /// - /// source - public static IObservable TrueForAll(this IObservable> source, - Func> observableSelector, - Func equalityCondition) + /// The items. + /// source. + [Obsolete(Constants.EvaluateIsDead)] + public static void Evaluate(this ISourceCache source, IEnumerable items) + where TKey : notnull { - return source.TrueFor(observableSelector, - items => items.All(o => o.LatestValue.HasValue && equalityCondition(o.Item, o.LatestValue.Value))); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + source.Edit(updater => updater.Refresh(items)); } /// - /// Produces a boolean observable indicating whether the resulting value of whether any of the specified observables matches - /// the equality condition. The observable is re-evaluated whenever - /// i) The cache changes. - /// or ii) The inner observable changes. + /// Signal observers to re-evaluate the all items. /// /// The type of the object. /// The type of the key. - /// The type of the value. /// The source. - /// The observable selector. - /// The equality condition. - /// - /// - /// source - /// or - /// observableSelector - /// or - /// equalityCondition - /// - public static IObservable TrueForAny(this IObservable> source, - Func> observableSelector, - Func equalityCondition) + /// source. + [Obsolete(Constants.EvaluateIsDead)] + public static void Evaluate(this ISourceCache source) + where TKey : notnull { - return source.TrueFor(observableSelector, - items => items.Any(o => o.LatestValue.HasValue && equalityCondition(o.Item, o.LatestValue.Value))); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + source.Edit(updater => updater.Refresh()); } /// - /// Produces a boolean observable indicating whether the resulting value of whether any of the specified observables matches - /// the equality condition. The observable is re-evaluated whenever - /// i) The cache changes. - /// or ii) The inner observable changes. + /// Dynamically apply a logical Except operator between the collections + /// Items from the first collection in the outer list are included unless contained in any of the other lists. /// /// The type of the object. /// The type of the key. - /// The type of the value. /// The source. - /// The observable selector. - /// The equality condition. - /// + /// The others. + /// An observable which emits change sets. /// /// source /// or - /// observableSelector - /// or - /// equalityCondition + /// others. /// - public static IObservable TrueForAny(this IObservable> source, - Func> observableSelector, - Func equalityCondition) + public static IObservable> Except(this IObservable> source, params IObservable>[] others) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (observableSelector == null) + if (others is null || others.Length == 0) { - throw new ArgumentNullException(nameof(observableSelector)); - } - - if (equalityCondition == null) - { - throw new ArgumentNullException(nameof(equalityCondition)); + throw new ArgumentNullException(nameof(others)); } - return source.TrueFor(observableSelector, - items => items.Any(o => o.LatestValue.HasValue && equalityCondition(o.LatestValue.Value))); - } - - private static IObservable TrueFor(this IObservable> source, - Func> observableSelector, - Func>, bool> collectionMatcher) - { - return new TrueFor(source, observableSelector, collectionMatcher).Run(); + return source.Combine(CombineOperator.Except, others); } - #endregion - - #region Entire Collection Operators - /// - /// The latest copy of the cache is exposed for querying after each modification to the underlying data + /// Dynamically apply a logical Except operator between the collections + /// Items from the first collection in the outer list are included unless contained in any of the other lists. /// /// The type of the object. /// The type of the key. - /// The type of the destination. - /// The source. - /// The result selector. - /// + /// The sources. + /// An observable which emits change sets. /// /// source /// or - /// resultSelector + /// others. /// - public static IObservable QueryWhenChanged(this IObservable> source, - Func, TDestination> resultSelector) + public static IObservable> Except(this ICollection>> sources) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (resultSelector == null) + if (sources is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(sources)); } - return source.QueryWhenChanged().Select(resultSelector); + return sources.Combine(CombineOperator.Except); } /// - /// The latest copy of the cache is exposed for querying i) after each modification to the underlying data ii) upon subscription + /// Dynamically apply a logical Except operator between the collections + /// Items from the first collection in the outer list are included unless contained in any of the other lists. /// /// The type of the object. /// The type of the key. - /// The source. - /// - /// source - public static IObservable> QueryWhenChanged(this IObservable> source) + /// The source. + /// An observable which emits change sets. + public static IObservable> Except(this IObservableList>> sources) + where TKey : notnull { - if (source == null) + if (sources is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(sources)); } - return new QueryWhenChanged(source).Run(); + return sources.Combine(CombineOperator.Except); } /// - /// The latest copy of the cache is exposed for querying i) after each modification to the underlying data ii) on subscription + /// Dynamically apply a logical Except operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The type of the value. - /// The source. - /// Should the query be triggered for observables on individual items - /// - /// source - public static IObservable> QueryWhenChanged([NotNull] this IObservable> source, - [NotNull] Func> itemChangedTrigger) + /// The source. + /// An observable which emits change sets. + public static IObservable> Except(this IObservableList> sources) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (itemChangedTrigger == null) + if (sources is null) { - throw new ArgumentNullException(nameof(itemChangedTrigger)); + throw new ArgumentNullException(nameof(sources)); } - return new QueryWhenChanged(source, itemChangedTrigger).Run(); + return sources.Combine(CombineOperator.Except); } /// - /// Converts the changeset into a fully formed collection. Each change in the source results in a new collection + /// Dynamically apply a logical Except operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The source. - /// - public static IObservable> ToCollection(this IObservable> source) + /// The source. + /// An observable which emits change sets. + public static IObservable> Except(this IObservableList> sources) + where TKey : notnull { - return source.QueryWhenChanged(query => new ReadOnlyCollectionLight(query.Items)); + if (sources is null) + { + throw new ArgumentNullException(nameof(sources)); + } + + return sources.Combine(CombineOperator.Except); } /// - /// Converts the changeset into a fully formed sorted collection. Each change in the source results in a new sorted collection + /// Automatically removes items from the stream after the time specified by + /// the timeSelector elapses. Return null if the item should never be removed. /// /// The type of the object. /// The type of the key. - /// The sort key /// The source. - /// The sort function - /// The sort order. Defaults to ascending - /// - public static IObservable> ToSortedCollection(this IObservable> source, - Func sort, SortDirection sortOrder = SortDirection.Ascending) + /// The time selector. + /// An observable which emits change sets. + /// + /// source + /// or + /// timeSelector. + /// + public static IObservable> ExpireAfter(this IObservable> source, Func timeSelector) + where TKey : notnull { - return source.QueryWhenChanged(query => sortOrder == SortDirection.Ascending - ? new ReadOnlyCollectionLight(query.Items.OrderBy(sort)) - : new ReadOnlyCollectionLight(query.Items.OrderByDescending(sort))); + return ExpireAfter(source, timeSelector, Scheduler.Default); } /// - /// Converts the changeset into a fully formed sorted collection. Each change in the source results in a new sorted collection + /// Automatically removes items from the stream after the time specified by + /// the timeSelector elapses. Return null if the item should never be removed. /// /// The type of the object. /// The type of the key. /// The source. - /// The sort comparer - /// - public static IObservable> ToSortedCollection(this IObservable> source, - IComparer comparer) + /// The time selector. + /// The scheduler. + /// An observable which emits change sets. + /// + /// source + /// or + /// timeSelector. + /// + public static IObservable> ExpireAfter(this IObservable> source, Func timeSelector, IScheduler scheduler) + where TKey : notnull { - return source.QueryWhenChanged(query => + if (source is null) { - var items = query.Items.AsList(); - items.Sort(comparer); - return new ReadOnlyCollectionLight(items); - }); - } + throw new ArgumentNullException(nameof(source)); + } - #endregion + if (timeSelector is null) + { + throw new ArgumentNullException(nameof(timeSelector)); + } - #region Watch + return source.ExpireAfter(timeSelector, null, scheduler); + } /// - /// Watches updates for a single value matching the specified key - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The key. - /// - /// source - public static IObservable WatchValue(this IObservableCache source, TKey key) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source.Watch(key).Select(u => u.Current); - } - - /// - /// Watches updates for a single value matching the specified key - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The key. - /// - /// source - public static IObservable WatchValue(this IObservable> source, TKey key) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source.Watch(key).Select(u => u.Current); - } - - /// - /// Returns an observable of any updates which match the specified key, preceeded with the initital cache state - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The key. - /// - public static IObservable> Watch(this IObservable> source, TKey key) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source.SelectMany(updates => updates).Where(update => update.Key.Equals(key)); - } - - #endregion - - #region Clone - - /// - /// Clones the changes into the specified collection - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The target. - /// - public static IObservable> Clone(this IObservable> source, [NotNull] ICollection target) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - return source.Do(changes => - { - foreach (var item in changes) - { - switch (item.Reason) - { - case ChangeReason.Add: - { - target.Add(item.Current); - } - - break; - case ChangeReason.Update: - { - target.Remove(item.Previous.Value); - target.Add(item.Current); - } - - break; - case ChangeReason.Remove: - target.Remove(item.Current); - break; - } - } - }); - } - - #endregion - - #region Auto removal - - /// - /// Automatically removes items from the stream after the time specified by - /// the timeSelector elapses. Return null if the item should never be removed - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The time selector. - /// - /// - /// source - /// or - /// timeSelector - /// - public static IObservable> ExpireAfter(this IObservable> source, - Func timeSelector) - { - return ExpireAfter(source, timeSelector, Scheduler.Default); - } - - /// - /// Automatically removes items from the stream after the time specified by - /// the timeSelector elapses. Return null if the item should never be removed - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The time selector. - /// The scheduler. - /// - /// - /// source - /// or - /// timeSelector - /// - public static IObservable> ExpireAfter(this IObservable> source, - Func timeSelector, IScheduler scheduler) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (timeSelector == null) - { - throw new ArgumentNullException(nameof(timeSelector)); - } - - return source.ExpireAfter(timeSelector, null, scheduler); - } - - /// - /// Automatically removes items from the stream on the next poll after the time specified by - /// the time selector elapses + /// Automatically removes items from the stream on the next poll after the time specified by + /// the time selector elapses. /// /// The type of the object. /// The type of the key. /// The cache. - /// The time selector. Return null if the item should never be removed + /// The time selector. Return null if the item should never be removed. /// The polling interval. if this value is specified, items are expired on an interval. /// This will result in a loss of accuracy of the time which the item is expired but is less computationally expensive. /// - /// An observable of anumerable of the kev values which has been removed + /// An observable of enumerable of the key values which has been removed. /// source /// or - /// timeSelector - public static IObservable> ExpireAfter(this IObservable> source, - Func timeSelector, TimeSpan? pollingInterval) + /// timeSelector. + public static IObservable> ExpireAfter(this IObservable> source, Func timeSelector, TimeSpan? pollingInterval) + where TKey : notnull { return ExpireAfter(source, timeSelector, pollingInterval, Scheduler.Default); } /// /// Automatically removes items from the stream on the next poll after the time specified by - /// the time selector elapses + /// the time selector elapses. /// /// The type of the object. /// The type of the key. /// The cache. - /// The time selector. Return null if the item should never be removed + /// The time selector. Return null if the item should never be removed. /// The polling interval. if this value is specified, items are expired on an interval. /// This will result in a loss of accuracy of the time which the item is expired but is less computationally expensive. /// /// The scheduler. - /// An observable of anumerable of the kev values which has been removed + /// An observable of enumerable of the key values which has been removed. /// source /// or - /// timeSelector - public static IObservable> ExpireAfter(this IObservable> source, - Func timeSelector, TimeSpan? pollingInterval, IScheduler scheduler) + /// timeSelector. + public static IObservable> ExpireAfter(this IObservable> source, Func timeSelector, TimeSpan? pollingInterval, IScheduler scheduler) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (timeSelector == null) + if (timeSelector is null) { throw new ArgumentNullException(nameof(timeSelector)); } @@ -1703,81 +1494,96 @@ public static IObservable> ExpireAfter( /// The type of the object. /// The type of the key. /// The cache. - /// The time selector. Return null if the item should never be removed - /// A polling interval. Since multiple timer subscriptions can be expensive, - /// it may be worth setting the interval. - /// - /// The scheduler. - /// An observable of anumerable of the kev values which has been removed + /// The time selector. Return null if the item should never be removed. + /// The scheduler to perform the work on. + /// An observable of enumerable of the key values which has been removed. /// source /// or - /// timeSelector - internal static IObservable>> ForExpiry(this IObservable> source, - Func timeSelector, - TimeSpan? interval, - IScheduler scheduler) + /// timeSelector. + public static IObservable>> ExpireAfter(this ISourceCache source, Func timeSelector, IScheduler? scheduler = null) + where TKey : notnull { - return new TimeExpirer(source, timeSelector, interval, scheduler).ForExpiry(); + return source.ExpireAfter(timeSelector, null, scheduler); } /// - /// Applies a size limiter to the number of records which can be included in the - /// underlying cache. When the size limit is reached the oldest items are removed. + /// Automatically removes items from the cache after the time specified by + /// the time selector elapses. /// /// The type of the object. /// The type of the key. - /// The source. - /// The size. - /// - /// source - /// size cannot be zero - public static IObservable> LimitSizeTo( - this IObservable> source, int size) + /// The cache. + /// The time selector. Return null if the item should never be removed. + /// A polling interval. Since multiple timer subscriptions can be expensive, + /// it may be worth setting the interval . + /// + /// An observable of enumerable of the key values which has been removed. + /// source + /// or + /// timeSelector. + public static IObservable>> ExpireAfter(this ISourceCache source, Func timeSelector, TimeSpan? interval = null) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (size <= 0) - { - throw new ArgumentException("Size limit must be greater than zero"); - } - - return new SizeExpirer(source, size).Run(); + return ExpireAfter(source, timeSelector, interval, Scheduler.Default); } - #endregion - - #region Paged - /// - /// Returns the page as specified by the pageRequests observable + /// Automatically removes items from the cache after the time specified by + /// the time selector elapses. /// /// The type of the object. /// The type of the key. - /// The source. - /// The page requests. - /// - public static IObservable> Page([NotNull] this IObservable> source, - [NotNull] IObservable pageRequests) + /// The cache. + /// The time selector. Return null if the item should never be removed. + /// A polling interval. Since multiple timer subscriptions can be expensive, + /// it may be worth setting the interval. + /// + /// The scheduler. + /// An observable of enumerable of the key values which has been removed. + /// source + /// or + /// timeSelector. + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Deliberate capture.")] + public static IObservable>> ExpireAfter(this ISourceCache source, Func timeSelector, TimeSpan? pollingInterval, IScheduler? scheduler) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (pageRequests == null) + if (timeSelector is null) { - throw new ArgumentNullException(nameof(pageRequests)); + throw new ArgumentNullException(nameof(timeSelector)); } - return new Page(source, pageRequests).Run(); - } - - #endregion + scheduler ??= Scheduler.Default; - #region Filter + return Observable.Create>>( + observer => + { + return source.Connect().ForExpiry(timeSelector, pollingInterval, scheduler).Finally(observer.OnCompleted).Subscribe( + toRemove => + { + try + { + // remove from cache and notify which items have been auto removed + var keyValuePairs = toRemove as KeyValuePair[] ?? toRemove.AsArray(); + if (keyValuePairs.Length == 0) + { + return; + } + + source.Remove(keyValuePairs.Select(kv => kv.Key)); + observer.OnNext(keyValuePairs); + } + catch (Exception ex) + { + observer.OnError(ex); + } + }); + }); + } /// /// Filters the specified source. @@ -1786,10 +1592,11 @@ public static IObservable> Page([N /// The type of the key. /// The source. /// The filter. - /// + /// An observable which emits change sets. public static IObservable> Filter(this IObservable> source, Func filter) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -1798,22 +1605,22 @@ public static IObservable> Filter(this } /// - /// Creates a filtered stream which can be dynamically filtered + /// Creates a filtered stream which can be dynamically filtered. /// /// The type of the object. /// The type of the key. /// The source. /// Observable to change the underlying predicate. - /// - public static IObservable> Filter([NotNull] this IObservable> source, - [NotNull] IObservable> predicateChanged) + /// An observable which emits change sets. + public static IObservable> Filter(this IObservable> source, IObservable> predicateChanged) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (predicateChanged == null) + if (predicateChanged is null) { throw new ArgumentNullException(nameof(predicateChanged)); } @@ -1822,22 +1629,22 @@ public static IObservable> Filter([NotN } /// - /// Creates a filtered stream which can be dynamically filtered + /// Creates a filtered stream which can be dynamically filtered. /// /// The type of the object. /// The type of the key. /// The source. - /// Observable to re-evaluate whether the filter still matches items. Use when filtering on mutable values - /// - public static IObservable> Filter([NotNull] this IObservable> source, - [NotNull] IObservable reapplyFilter) + /// Observable to re-evaluate whether the filter still matches items. Use when filtering on mutable values. + /// An observable which emits change sets. + public static IObservable> Filter(this IObservable> source, IObservable reapplyFilter) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (reapplyFilter == null) + if (reapplyFilter is null) { throw new ArgumentNullException(nameof(reapplyFilter)); } @@ -1847,29 +1654,28 @@ public static IObservable> Filter([NotN } /// - /// Creates a filtered stream which can be dynamically filtered + /// Creates a filtered stream which can be dynamically filtered. /// /// The type of the object. /// The type of the key. /// The source. - /// Observable to re-evaluate whether the filter still matches items. Use when filtering on mutable values /// Observable to change the underlying predicate. - /// - public static IObservable> Filter([NotNull] this IObservable> source, - [NotNull] IObservable> predicateChanged, - [NotNull] IObservable reapplyFilter) + /// Observable to re-evaluate whether the filter still matches items. Use when filtering on mutable values. + /// An observable which emits change sets. + public static IObservable> Filter(this IObservable> source, IObservable> predicateChanged, IObservable reapplyFilter) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (predicateChanged == null) + if (predicateChanged is null) { throw new ArgumentNullException(nameof(predicateChanged)); } - if (reapplyFilter == null) + if (reapplyFilter is null) { throw new ArgumentNullException(nameof(reapplyFilter)); } @@ -1880,38 +1686,33 @@ public static IObservable> Filter([NotN /// /// Filters source on the specified property using the specified predicate. /// The filter will automatically reapply when a property changes. - /// When there are likely to be a large number of property changes specify a throttle to improve performance + /// When there are likely to be a large number of property changes specify a throttle to improve performance. /// /// The type of the object. /// The type of the key. /// The type of the property. /// The source. - /// The property selector. When the property changes a the filter specified will be re-evaluated - /// A predicate based on the object which contains the changed property + /// The property selector. When the property changes a the filter specified will be re-evaluated. + /// A predicate based on the object which contains the changed property. /// The property changed throttle. - /// The scheduler used when throttling - /// - /// + /// The scheduler used when throttling. + /// An observable which emits change sets. [Obsolete("Use AutoRefresh(), followed by Filter() instead")] - public static IObservable> FilterOnProperty( - this IObservable> source, - Expression> propertySelector, - Func predicate, - TimeSpan? propertyChangedThrottle = null, - IScheduler scheduler = null) + public static IObservable> FilterOnProperty(this IObservable> source, Expression> propertySelector, Func predicate, TimeSpan? propertyChangedThrottle = null, IScheduler? scheduler = null) + where TKey : notnull where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertySelector == null) + if (propertySelector is null) { throw new ArgumentNullException(nameof(propertySelector)); } - if (predicate == null) + if (predicate is null) { throw new ArgumentNullException(nameof(predicate)); } @@ -1919,3931 +1720,3925 @@ public static IObservable> FilterOnProperty(source, propertySelector, predicate, propertyChangedThrottle, scheduler).Run(); } - #endregion - - #region Interface aware - /// - /// Updates the index for an object which implements IIndexAware + /// Ensure that finally is always called. Thanks to Lee Campbell for this. /// - /// The type of the object. - /// The type of the key. + /// The type contained within the observables. /// The source. - /// - public static IObservable> UpdateIndex(this IObservable> source) - where TObject : IIndexAware + /// The finally action. + /// An observable which has always a finally action applied. + /// source. + [Obsolete("This can cause unhandled exception issues so do not use")] + public static IObservable FinallySafe(this IObservable source, Action finallyAction) { - return source.Do(changes => changes.SortedItems.Select((update, index) => new { update, index }) - .ForEach(u => u.update.Value.Index = u.index)); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (finallyAction is null) + { + throw new ArgumentNullException(nameof(finallyAction)); + } + + return new FinallySafe(source, finallyAction).Run(); } /// - /// Invokes Refresh method for an object which implements IEvaluateAware + /// Flattens an update collection to it's individual items. /// /// The type of the object. /// The type of the key. /// The source. - /// - public static IObservable> InvokeEvaluate(this IObservable> source) - where TObject : IEvaluateAware + /// An observable which emits change set values on a flatten result. + /// source. + public static IObservable> Flatten(this IObservable> source) + where TKey : notnull { - return source.Do(changes => changes.Where(u => u.Reason == ChangeReason.Refresh).ForEach(u => u.Current.Evaluate())); - } - - #endregion - - #region Sort + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - private const int DefaultSortResetThreshold = 100; + return source.SelectMany(changes => changes); + } /// - /// Sorts using the specified comparer. - /// Returns the underlying ChangeSet as as per the system conventions. - /// The resulting changeset also exposes a sorted key value collection of of the underlying cached data + /// Convert the result of a buffer operation to a single change set. /// /// The type of the object. /// The type of the key. /// The source. - /// The comparer. - /// Sort optimisation flags. Specify one or more sort optimisations - /// The number of updates before the entire list is resorted (rather than inline sort) - /// - /// - /// source - /// or - /// comparer - /// - public static IObservable> Sort(this IObservable> source, - IComparer comparer, - SortOptimisations sortOptimisations = SortOptimisations.None, - int resetThreshold = DefaultSortResetThreshold) + /// An observable which emits change sets. + public static IObservable> FlattenBufferResult(this IObservable>> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (comparer == null) - { - throw new ArgumentNullException(nameof(comparer)); - } - - return new Sort(source, comparer, sortOptimisations, resetThreshold: resetThreshold).Run(); + return source.Where(x => x.Count != 0).Select(updates => new ChangeSet(updates.SelectMany(u => u))); } /// - /// Sorts a sequence as, using the comparer observable to determine order + /// Provides a call back for each change. /// /// The type of the object. /// The type of the key. /// The source. - /// The comparer observable. - /// The sort optimisations. - /// The reset threshold. - /// - public static IObservable> Sort(this IObservable> source, - IObservable> comparerObservable, - SortOptimisations sortOptimisations = SortOptimisations.None, - int resetThreshold = DefaultSortResetThreshold) + /// The action. + /// An observable which will perform the action on each item. + public static IObservable> ForEachChange(this IObservable> source, Action> action) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (comparerObservable == null) + if (action is null) { - throw new ArgumentNullException(nameof(comparerObservable)); + throw new ArgumentNullException(nameof(action)); } - return new Sort(source, null, sortOptimisations, comparerObservable, resetThreshold: resetThreshold).Run(); + return source.Do(changes => changes.ForEach(action)); } /// - /// Sorts a sequence as, using the comparer observable to determine order + /// Joins the left and right observable data sources, taking any left or right values and matching them, provided that the left or the right has a value. + /// This is the equivalent of SQL full join. /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The comparer observable. - /// Signal to instruct the algroirthm to re-sort the entire data set - /// The sort optimisations. - /// The reset threshold. - /// - public static IObservable> Sort(this IObservable> source, - IObservable> comparerObservable, - IObservable resorter, - SortOptimisations sortOptimisations = SortOptimisations.None, - int resetThreshold = DefaultSortResetThreshold) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> FullJoin(this IObservable> left, IObservable> right, Func rightKeySelector, Func, Optional, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(left)); } - if (comparerObservable == null) + if (right is null) { - throw new ArgumentNullException(nameof(comparerObservable)); + throw new ArgumentNullException(nameof(right)); } - return new Sort(source, null, sortOptimisations, comparerObservable, resorter, resetThreshold).Run(); + if (rightKeySelector is null) + { + throw new ArgumentNullException(nameof(rightKeySelector)); + } + + if (resultSelector is null) + { + throw new ArgumentNullException(nameof(resultSelector)); + } + + return left.FullJoin(right, rightKeySelector, (_, leftValue, rightValue) => resultSelector(leftValue, rightValue)); } /// - /// Sorts a sequence as, using the comparer observable to determine order + /// Joins the left and right observable data sources, taking any left or right values and matching them, provided that the left or the right has a value. + /// This is the equivalent of SQL full join. /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The comparer to sort on - /// Signal to instruct the algroirthm to re-sort the entire data set - /// The sort optimisations. - /// The reset threshold. - /// - public static IObservable> Sort(this IObservable> source, - IComparer comparer, - IObservable resorter, - SortOptimisations sortOptimisations = SortOptimisations.None, - int resetThreshold = DefaultSortResetThreshold) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> FullJoin(this IObservable> left, IObservable> right, Func rightKeySelector, Func, Optional, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(left)); } - if (resorter == null) + if (right is null) { - throw new ArgumentNullException(nameof(resorter)); + throw new ArgumentNullException(nameof(right)); } - return new Sort(source, comparer, sortOptimisations, null, resorter, resetThreshold).Run(); - } + if (rightKeySelector is null) + { + throw new ArgumentNullException(nameof(rightKeySelector)); + } - /// - /// Converts moves changes to remove + add - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// the same SortedChangeSets, except all moves are replaced with remove + add. - public static IObservable> TreatMovesAsRemoveAdd( - this IObservable> source) - { - if (source == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(resultSelector)); } - IEnumerable> ReplaceMoves(IChangeSet items) - { - foreach (var change in items) - { - if (change.Reason == ChangeReason.Moved) - { - yield return new Change( - ChangeReason.Remove, - change.Key, - change.Current, change.PreviousIndex); - - yield return new Change( - ChangeReason.Add, - change.Key, - change.Current, - change.CurrentIndex); - } - else - { - yield return change; - } - } - } - - return source.Select(changes => new SortedChangeSet(changes.SortedItems, ReplaceMoves(changes))); - } - - #endregion - - #region And, or, except + return new FullJoin(left, right, rightKeySelector, resultSelector).Run(); + } /// - /// Applied a logical And operator between the collections i.e items which are in all of the sources are included + /// Groups the right data source and joins the resulting group to the left data source, matching these using the specified key selector. Results are included when the left or the right has a value. + /// This is the equivalent of SQL full join. /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The others. - /// - /// - /// source - /// or - /// others - /// - public static IObservable> And(this IObservable> source, - params IObservable>[] others) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> FullJoinMany(this IObservable> left, IObservable> right, Func rightKeySelector, Func, IGrouping, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(left)); } - if (others == null || others.Length == 0) + if (right is null) { - throw new ArgumentNullException(nameof(others)); + throw new ArgumentNullException(nameof(right)); } - return source.Combine(CombineOperator.And, others); - } + if (rightKeySelector is null) + { + throw new ArgumentNullException(nameof(rightKeySelector)); + } - /// - /// Applied a logical And operator between the collections i.e items which are in all of the sources are included - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - /// - /// source - /// or - /// others - /// - public static IObservable> And(this ICollection>> sources) - { - if (sources == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(resultSelector)); } - return sources.Combine(CombineOperator.And); + return left.FullJoinMany(right, rightKeySelector, (_, leftValue, rightValue) => resultSelector(leftValue, rightValue)); } /// - /// Dynamically apply a logical And operator between the items in the outer observable list. - /// Items which are in all of the sources are included in the result + /// Groups the right data source and joins the resulting group to the left data source, matching these using the specified key selector. Results are included when the left or the right has a value. + /// This is the equivalent of SQL full join. /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> And(this IObservableList>> sources) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> FullJoinMany(this IObservable> left, IObservable> right, Func rightKeySelector, Func, IGrouping, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (sources == null) + if (left is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(left)); } - return sources.Combine(CombineOperator.And); - } + if (right is null) + { + throw new ArgumentNullException(nameof(right)); + } - /// - /// Dynamically apply a logical And operator between the items in the outer observable list. - /// Items which are in all of the sources are included in the result - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> And(this IObservableList> sources) - { - if (sources == null) + if (rightKeySelector is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - return sources.Combine(CombineOperator.And); + if (resultSelector is null) + { + throw new ArgumentNullException(nameof(resultSelector)); + } + + return new FullJoinMany(left, right, rightKeySelector, resultSelector).Run(); } /// - /// Dynamically apply a logical And operator between the items in the outer observable list. - /// Items which are in all of the sources are included in the result + /// Groups the source on the value returned by group selector factory. + /// A group is included for each item in the resulting group source. /// /// The type of the object. /// The type of the key. - /// The source. - /// - public static IObservable> And(this IObservableList> sources) + /// The type of the group key. + /// The source. + /// The group selector factory. + /// + /// A distinct stream used to determine the result. + /// + /// + /// Useful for parent-child collection when the parent and child are soured from different streams. + /// + /// An observable which will emit group change sets. + public static IObservable> Group(this IObservable> source, Func groupSelector, IObservable> resultGroupSource) + where TKey : notnull + where TGroupKey : notnull { - if (sources == null) + if (source is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(source)); } - return sources.Combine(CombineOperator.And); + if (groupSelector is null) + { + throw new ArgumentNullException(nameof(groupSelector)); + } + + if (resultGroupSource is null) + { + throw new ArgumentNullException(nameof(resultGroupSource)); + } + + return new SpecifiedGrouper(source, groupSelector, resultGroupSource).Run(); } /// - /// Apply a logical Or operator between the collections i.e items which are in any of the sources are included + /// Groups the source on the value returned by group selector factory. /// /// The type of the object. /// The type of the key. + /// The type of the group key. /// The source. - /// The others. - /// - /// - /// source - /// or - /// others - /// - public static IObservable> Or(this IObservable> source, - params IObservable>[] others) + /// The group selector key. + /// An observable which will emit group change sets. + public static IObservable> Group(this IObservable> source, Func groupSelectorKey) + where TKey : notnull + where TGroupKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (others == null || others.Length == 0) + if (groupSelectorKey is null) { - throw new ArgumentNullException(nameof(others)); + throw new ArgumentNullException(nameof(groupSelectorKey)); } - return source.Combine(CombineOperator.Or, others); + return new GroupOn(source, groupSelectorKey, null).Run(); } /// - /// Apply a logical Or operator between the collections i.e items which are in any of the sources are included + /// Groups the source on the value returned by group selector factory. /// /// The type of the object. /// The type of the key. - /// The source. - /// + /// The type of the group key. + /// The source. + /// The group selector key. + /// Invoke to the for the grouping to be re-evaluated. + /// An observable which will emit group change sets. /// /// source /// or - /// others + /// groupSelectorKey + /// or + /// groupController. /// - public static IObservable> Or(this ICollection>> sources) + public static IObservable> Group(this IObservable> source, Func groupSelectorKey, IObservable regrouper) + where TKey : notnull + where TGroupKey : notnull { - if (sources == null) + if (source is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(source)); } - return sources.Combine(CombineOperator.Or); - } + if (groupSelectorKey is null) + { + throw new ArgumentNullException(nameof(groupSelectorKey)); + } - /// - /// Dynamically apply a logical Or operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> Or(this IObservableList>> sources) - { - if (sources == null) + if (regrouper is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(regrouper)); } - return sources.Combine(CombineOperator.Or); + return new GroupOn(source, groupSelectorKey, regrouper).Run(); } - //public static IObservable> Or(this IObservable> source, - // Func>> manyselector) - //{ - // return new TransformMany(source, manyselector, keySelector).Run(); - //} - - //public static IObservable> Or(this IObservable> source, - // Func>> manyselector) - //{ - // return new TransformMany(source, manyselector, keySelector).Run(); - //} - - //public static IObservable> TransformMany(this IObservable> source, - // Func> manyselector) - //{ - // return new TransformMany(source, manyselector, keySelector).Run(); - //} - - //public static IObservable> Or(this IObservable> sources) - //{ - // if (sources == null) throw new ArgumentNullException(nameof(sources)); - - // return sources.Combine(CombineOperator.Or); - //} - /// - /// Dynamically apply a logical Or operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result + /// Groups the source using the property specified by the property selector. Groups are re-applied when the property value changed. + /// + /// When there are likely to be a large number of group property changes specify a throttle to improve performance. /// /// The type of the object. /// The type of the key. - /// The source. - /// - public static IObservable> Or(this IObservableList> sources) + /// The type of the group key. + /// The source. + /// The property selector used to group the items. + /// A time span that indicates the throttle to wait for property change events. + /// The scheduler. + /// An observable which will emit immutable group change sets. + public static IObservable> GroupOnProperty(this IObservable> source, Expression> propertySelector, TimeSpan? propertyChangedThrottle = null, IScheduler? scheduler = null) + where TObject : INotifyPropertyChanged + where TKey : notnull + where TGroupKey : notnull { - if (sources == null) + if (source is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(source)); } - return sources.Combine(CombineOperator.Or); - } - - /// - /// Dynamically apply a logical Or operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> Or(this IObservableList> sources) - { - if (sources == null) + if (propertySelector is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(propertySelector)); } - return sources.Combine(CombineOperator.Or); + return new GroupOnProperty(source, propertySelector, propertyChangedThrottle, scheduler).Run(); } /// - /// Apply a logical Xor operator between the collections. - /// Items which are only in one of the sources are included in the result + /// Groups the source using the property specified by the property selector. Each update produces immutable grouping. Groups are re-applied when the property value changed. + /// + /// When there are likely to be a large number of group property changes specify a throttle to improve performance. /// /// The type of the object. /// The type of the key. + /// The type of the group key. /// The source. - /// The others. - /// - /// - /// source - /// or - /// others - /// - public static IObservable> Xor(this IObservable> source, - params IObservable>[] others) + /// The property selector used to group the items. + /// A time span that indicates the throttle to wait for property change events. + /// The scheduler. + /// An observable which will emit immutable group change sets. + public static IObservable> GroupOnPropertyWithImmutableState(this IObservable> source, Expression> propertySelector, TimeSpan? propertyChangedThrottle = null, IScheduler? scheduler = null) + where TObject : INotifyPropertyChanged + where TKey : notnull + where TGroupKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (others == null || others.Length == 0) + if (propertySelector is null) { - throw new ArgumentNullException(nameof(others)); + throw new ArgumentNullException(nameof(propertySelector)); } - return source.Combine(CombineOperator.Xor, others); + return new GroupOnPropertyWithImmutableState(source, propertySelector, propertyChangedThrottle, scheduler).Run(); } /// - /// Apply a logical Xor operator between the collections. - /// Items which are only in one of the sources are included in the result + /// Groups the source on the value returned by group selector factory. Each update produces immutable grouping. /// /// The type of the object. /// The type of the key. - /// The source. - /// + /// The type of the group key. + /// The source. + /// The group selector key. + /// Invoke to the for the grouping to be re-evaluated. + /// An observable which will emit immutable group change sets. /// /// source /// or - /// others + /// groupSelectorKey + /// or + /// groupController. /// - public static IObservable> Xor(this ICollection>> sources) + public static IObservable> GroupWithImmutableState(this IObservable> source, Func groupSelectorKey, IObservable? regrouper = null) + where TKey : notnull + where TGroupKey : notnull { - if (sources == null) + if (source is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(source)); } - return sources.Combine(CombineOperator.Xor); - } - - /// - /// Dynamically apply a logical Xor operator between the items in the outer observable list. - /// Items which are only in one of the sources are included in the result - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> Xor(this IObservableList>> sources) - { - if (sources == null) + if (groupSelectorKey is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(groupSelectorKey)); } - return sources.Combine(CombineOperator.Xor); + return new GroupOnImmutable(source, groupSelectorKey, regrouper).Run(); } /// - /// Dynamically apply a logical Xor operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result + /// Ignores updates when the update is the same reference. /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> Xor(this IObservableList> sources) + /// The object of the change set. + /// The key of the change set. + /// The source observable which emits change sets. + /// An observable which emits change sets and ignores equal value changes. + public static IObservable> IgnoreSameReferenceUpdate(this IObservable> source) + where TKey : notnull { - if (sources == null) - { - throw new ArgumentNullException(nameof(sources)); - } - - return sources.Combine(CombineOperator.Xor); + return source.IgnoreUpdateWhen((c, p) => ReferenceEquals(c, p)); } /// - /// Dynamically apply a logical Xor operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result + /// Ignores the update when the condition is met. + /// The first parameter in the ignore function is the current value and the second parameter is the previous value. /// /// The type of the object. /// The type of the key. - /// The source. - /// - public static IObservable> Xor(this IObservableList> sources) + /// The source. + /// The ignore function (current,previous)=>{ return true to ignore }. + /// An observable which emits change sets and ignores updates equal to the lambda. + public static IObservable> IgnoreUpdateWhen(this IObservable> source, Func ignoreFunction) + where TKey : notnull { - if (sources == null) - { - throw new ArgumentNullException(nameof(sources)); - } + return source.Select( + updates => + { + var result = updates.Where( + u => + { + if (u.Reason != ChangeReason.Update) + { + return true; + } - return sources.Combine(CombineOperator.Xor); + return !ignoreFunction(u.Current, u.Previous.Value); + }); + return new ChangeSet(result); + }).NotEmpty(); } /// - /// Dynamically apply a logical Except operator between the collections - /// Items from the first collection in the outer list are included unless contained in any of the other lists + /// Only includes the update when the condition is met. + /// The first parameter in the ignore function is the current value and the second parameter is the previous value. /// /// The type of the object. /// The type of the key. /// The source. - /// The others. - /// - /// - /// source - /// or - /// others - /// - public static IObservable> Except(this IObservable> source, - params IObservable>[] others) + /// The include function (current,previous)=>{ return true to include }. + /// An observable which emits change sets and ignores updates equal to the lambda. + public static IObservable> IncludeUpdateWhen(this IObservable> source, Func includeFunction) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (others == null || others.Length == 0) + if (includeFunction is null) { - throw new ArgumentNullException(nameof(others)); + throw new ArgumentNullException(nameof(includeFunction)); } - return source.Combine(CombineOperator.Except, others); + return source.Select( + changes => + { + var result = changes.Where(change => change.Reason != ChangeReason.Update || includeFunction(change.Current, change.Previous.Value)); + return new ChangeSet(result); + }).NotEmpty(); } /// - /// Dynamically apply a logical Except operator between the collections - /// Items from the first collection in the outer list are included unless contained in any of the other lists + /// Joins the left and right observable data sources, taking values when both left and right values are present + /// This is the equivalent of SQL inner join. /// - /// The type of the object. - /// The type of the key. - /// The sources. - /// - /// - /// source - /// or - /// others - /// - public static IObservable> Except(this ICollection>> sources) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> InnerJoin(this IObservable> left, IObservable> right, Func rightKeySelector, Func resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (sources == null) + if (left is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(left)); } - return sources.Combine(CombineOperator.Except); - } - - /// - /// Dynamically apply a logical Except operator between the collections - /// Items from the first collection in the outer list are included unless contained in any of the other lists - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> Except(this IObservableList>> sources) - { - if (sources == null) + if (right is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(right)); } - return sources.Combine(CombineOperator.Except); - } - - /// - /// Dynamically apply a logical Except operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> Except(this IObservableList> sources) - { - if (sources == null) + if (rightKeySelector is null) { - throw new ArgumentNullException(nameof(sources)); - } - - return sources.Combine(CombineOperator.Except); - } - - /// - /// Dynamically apply a logical Except operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - public static IObservable> Except(this IObservableList> sources) - { - if (sources == null) - { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - return sources.Combine(CombineOperator.Except); - } - - private static IObservable> Combine([NotNull] this IObservableList> source, CombineOperator type) - { - if (source == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(resultSelector)); } - return Observable.Create>(observer => - { - var connections = source.Connect().Transform(x => x.Connect()).AsObservableList(); - var subscriber = connections.Combine(type).SubscribeSafe(observer); - return new CompositeDisposable(connections, subscriber); - }); + return left.InnerJoin(right, rightKeySelector, (_, leftValue, rightValue) => resultSelector(leftValue, rightValue)); } - private static IObservable> Combine([NotNull] this IObservableList> source, CombineOperator type) + /// + /// Groups the right data source and joins the to the left and the right sources, taking values when both left and right values are present + /// This is the equivalent of SQL inner join. + /// + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> InnerJoin(this IObservable> left, IObservable> right, Func rightKeySelector, Func resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(left)); } - return Observable.Create>(observer => + if (right is null) { - var connections = source.Connect().Transform(x => x.Connect()).AsObservableList(); - var subscriber = connections.Combine(type).SubscribeSafe(observer); - return new CompositeDisposable(connections, subscriber); - }); - } - - private static IObservable> Combine([NotNull] this IObservableList>> source, CombineOperator type) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(right)); } - return new DynamicCombiner(source, type).Run(); - } - - private static IObservable> Combine(this ICollection>> sources, CombineOperator type) - { - if (sources == null) + if (rightKeySelector is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - return Observable.Create> - ( - observer => - { - void UpdateAction(IChangeSet updates) - { - try - { - observer.OnNext(updates); - } - catch (Exception ex) - { - observer.OnError(ex); - } - } - - IDisposable subscriber = Disposable.Empty; - try - { - var combiner = new Combiner(type, UpdateAction); - subscriber = combiner.Subscribe(sources.ToArray()); - } - catch (Exception ex) - { - observer.OnError(ex); - observer.OnCompleted(); - } - - return subscriber; - }); - } - - private static IObservable> Combine(this IObservable> source, - CombineOperator type, - params IObservable>[] combinetarget) - { - if (combinetarget == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(combinetarget)); + throw new ArgumentNullException(nameof(resultSelector)); } - return Observable.Create> - ( - observer => - { - void UpdateAction(IChangeSet updates) - { - try - { - observer.OnNext(updates); - } - catch (Exception ex) - { - observer.OnError(ex); - observer.OnCompleted(); - } - } - - IDisposable subscriber = Disposable.Empty; - try - { - var list = combinetarget.ToList(); - list.Insert(0, source); - - var combiner = new Combiner(type, UpdateAction); - subscriber = combiner.Subscribe(list.ToArray()); - } - catch (Exception ex) - { - observer.OnError(ex); - observer.OnCompleted(); - } - - return subscriber; - }); + return new InnerJoin(left, right, rightKeySelector, resultSelector).Run(); } /// - /// The equivalent of rx startwith operator, but wraps the item in a change where reason is ChangeReason.Add + /// Groups the right data source and joins the resulting group to the left data source, matching these using the specified key selector. Results are included when the left and right have matching values. + /// This is the equivalent of SQL inner join. /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The item. - /// - public static IObservable> StartWithItem(this IObservable> source, - TObject item) where TObject : IKey + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> InnerJoinMany(this IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(left)); } - return source.StartWithItem(item, item.Key); - } - - /// - /// The equivalent of rx startwith operator, but wraps the item in a change where reason is ChangeReason.Add - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The item. - /// The key. - /// - public static IObservable> StartWithItem(this IObservable> source, - TObject item, TKey key) - { - if (source == null) + if (right is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(right)); } - var change = new Change(ChangeReason.Add, key, item); - return source.StartWith(new ChangeSet{change}); - } - - #endregion - - #region Transform - - /// - /// Projects each update item to a new form using the specified transform function - /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Should a new transform be applied when a refresh event is received - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, - Func transformFactory, - bool transformOnRefresh) - { - if (source == null) + if (rightKeySelector is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - if (transformFactory == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(resultSelector)); } - return source.Transform((current, previous, key) => transformFactory(current), transformOnRefresh); + return left.InnerJoinMany(right, rightKeySelector, (_, leftValue, rightValue) => resultSelector(leftValue, rightValue)); } /// - /// Projects each update item to a new form using the specified transform function + /// Groups the right data source and joins the resulting group to the left data source, matching these using the specified key selector. Results are included when the left and right have matching values. + /// This is the equivalent of SQL inner join. /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Should a new transform be applied when a refresh event is received - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, - Func transformFactory, - bool transformOnRefresh) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> InnerJoinMany(this IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(left)); } - if (transformFactory == null) + if (right is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(right)); } - return source.Transform((current, previous, key) => transformFactory(current, key), transformOnRefresh); - } - - /// - /// Projects each update item to a new form using the specified transform function - /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Should a new transform be applied when a refresh event is received - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, - Func, TKey, TDestination> transformFactory, - bool transformOnRefresh) - { - if (source == null) + if (rightKeySelector is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - if (transformFactory == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(resultSelector)); } - return new Transform(source, transformFactory, transformOnRefresh: transformOnRefresh).Run(); + return new InnerJoinMany(left, right, rightKeySelector, resultSelector).Run(); } /// - /// Projects each update item to a new form using the specified transform function + /// Invokes Refresh method for an object which implements IEvaluateAware. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, Func transformFactory, IObservable> forceTransform = null) + /// An observable which emits change sets. + public static IObservable> InvokeEvaluate(this IObservable> source) + where TObject : IEvaluateAware + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (transformFactory == null) - { - throw new ArgumentNullException(nameof(transformFactory)); - } - - return source.Transform((current, previous, key) => transformFactory(current), forceTransform.ForForced()); + return source.Do(changes => changes.Where(u => u.Reason == ChangeReason.Refresh).ForEach(u => u.Current.Evaluate())); } /// - /// Projects each update item to a new form using the specified transform function + /// Joins the left and right observable data sources, taking all left values and combining any matching right values. /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, Func transformFactory, IObservable> forceTransform = null) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> LeftJoin(this IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(left)); } - if (transformFactory == null) + if (right is null) { - throw new ArgumentNullException(nameof(transformFactory)); - } - - return source.Transform((current, previous, key) => transformFactory(current, key), forceTransform); - } - - /// - /// Projects each update item to a new form using the specified transform function - /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, Func, TKey, TDestination> transformFactory, IObservable> forceTransform = null) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(right)); } - if (transformFactory == null) + if (rightKeySelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - if (forceTransform!=null) + if (resultSelector is null) { - return new TransformWithForcedTransform(source, transformFactory, forceTransform).Run(); + throw new ArgumentNullException(nameof(resultSelector)); } - return new Transform(source, transformFactory).Run(); + return left.LeftJoin(right, rightKeySelector, (_, leftValue, rightValue) => resultSelector(leftValue, rightValue)); } /// - /// Projects each update item to a new form using the specified transform function + /// Joins the left and right observable data sources, taking all left values and combining any matching right values. /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Invoke to force a new transform for all items - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, Func transformFactory, IObservable forceTransform) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> LeftJoin(this IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - return source.Transform((cur, prev, key) => transformFactory(cur), forceTransform.ForForced()); - } + if (left is null) + { + throw new ArgumentNullException(nameof(left)); + } - /// - /// Projects each update item to a new form using the specified transform function - /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Invoke to force a new transform for all items# - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, Func transformFactory, IObservable forceTransform) - { - if (source == null) + if (right is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(right)); } - if (transformFactory == null) + if (rightKeySelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - if (forceTransform == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(forceTransform)); + throw new ArgumentNullException(nameof(resultSelector)); } - return source.Transform((cur, prev, key) => transformFactory(cur, key), forceTransform.ForForced()); + return new LeftJoin(left, right, rightKeySelector, resultSelector).Run(); } /// - /// Projects each update item to a new form using the specified transform function + /// Groups the right data source and joins the two sources matching them using the specified key selector, taking all left values and combining any matching right values. + /// This is the equivalent of SQL left join. /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Invoke to force a new transform for all items# - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, Func, TKey, TDestination> transformFactory, IObservable forceTransform) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> LeftJoinMany(this IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); - } - - if (transformFactory == null) - { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(left)); } - if (forceTransform == null) + if (right is null) { - throw new ArgumentNullException(nameof(forceTransform)); + throw new ArgumentNullException(nameof(right)); } - return source.Transform(transformFactory, forceTransform.ForForced()); - } - - private static IObservable> ForForced(this IObservable source) - { - return source?.Select(_ => + if (rightKeySelector is null) { - bool Transformer(TSource item, TKey key) => true; - return (Func) Transformer; - }); - } - - private static IObservable> ForForced(this IObservable> source) - { - return source?.Select(condition => - { - bool Transformer(TSource item, TKey key) => condition(item); - return (Func) Transformer; - }); - } - - #endregion - - #region Transform Async - - /// - /// Projects each update item to a new form using the specified transform function - /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// The maximum concurrent tasks used to perform transforms. - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> TransformAsync(this IObservable> source, - Func> transformFactory, - IObservable> forceTransform = null, - int maximumConcurrency = 1) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - if (transformFactory == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(resultSelector)); } - return source.TransformAsync((current, previous, key) => transformFactory(current), maximumConcurrency, forceTransform); + return left.LeftJoinMany(right, rightKeySelector, (_, leftValue, rightValue) => resultSelector(leftValue, rightValue)); } /// - /// Projects each update item to a new form using the specified transform function + /// Groups the right data source and joins the two sources matching them using the specified key selector, taking all left values and combining any matching right values. + /// This is the equivalent of SQL left join. /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// ***Concurrency has been disabled and will be re-implemented later. The maximum concurrent tasks used to perform transforms. - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> TransformAsync(this IObservable> source, - Func> transformFactory, - int maximumConcurrency = 1, - IObservable> forceTransform = null) + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. + /// The right data source. + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> LeftJoinMany(this IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (source == null) + if (left is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(left)); } - if (transformFactory == null) + if (right is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(right)); } - return source.TransformAsync((current, previous, key) => transformFactory(current, key), maximumConcurrency, forceTransform); - } - - /// - /// Projects each update item to a new form using the specified transform function - /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. - /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// ***Concurrency has been disabled and will be re-implemented later. The maximum concurrent tasks used to perform transforms. - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> TransformAsync(this IObservable> source, - Func, TKey, Task> transformFactory, - int maximumConcurrency = 1, - IObservable> forceTransform = null) - { - if (source == null) + if (rightKeySelector is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(rightKeySelector)); } - if (transformFactory == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(resultSelector)); } - return new TransformAsync(source, transformFactory, null, maximumConcurrency, forceTransform).Run(); - } - - #endregion - - #region Transform many - - /// - /// Equivalent to a select many transform. To work, the key must individually identify each child. - /// - /// The type of the destination. - /// The type of the destination key. - /// The type of the source. - /// The type of the source key. - /// The source. - /// The manyselector. - /// The key selector which must be unique across all - public static IObservable> TransformMany( - this IObservable> source, - Func> manyselector, - Func keySelector) - { - return new TransformMany(source, manyselector, keySelector).Run(); + return new LeftJoinMany(left, right, rightKeySelector, resultSelector).Run(); } /// - /// Flatten the nested observable collection, and subsequently observe observable collection changes + /// Applies a size limiter to the number of records which can be included in the + /// underlying cache. When the size limit is reached the oldest items are removed. /// - /// The type of the destination. - /// The type of the destination key. - /// The type of the source. - /// The type of the source key. + /// The type of the object. + /// The type of the key. /// The source. - /// The manyselector. - /// The key selector which must be unique across all - public static IObservable> TransformMany( - this IObservable> source, - Func> manyselector, - Func keySelector) + /// The size. + /// An observable which emits change sets. + /// source. + /// size cannot be zero. + public static IObservable> LimitSizeTo(this IObservable> source, int size) + where TKey : notnull { - return new TransformMany(source, manyselector, keySelector).Run(); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (size <= 0) + { + throw new ArgumentException("Size limit must be greater than zero"); + } + + return new SizeExpirer(source, size).Run(); } /// - /// Flatten the nested observable collection, and subsequently observe observable collection changes + /// Limits the number of records in the cache to the size specified. When the size is reached + /// the oldest items are removed from the cache. /// - /// The type of the destination. - /// The type of the destination key. - /// The type of the source. - /// The type of the source key. + /// The type of the object. + /// The type of the key. /// The source. - /// The manyselector. - /// The key selector which must be unique across all - public static IObservable> TransformMany( this IObservable> source, - Func> manyselector, - Func keySelector) + /// The size limit. + /// The scheduler. + /// An observable which emits the key value pairs. + /// source. + /// Size limit must be greater than zero. + public static IObservable>> LimitSizeTo(this ISourceCache source, int sizeLimit, IScheduler? scheduler = null) + where TKey : notnull { - return new TransformMany(source, manyselector, keySelector).Run(); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - #endregion + if (sizeLimit <= 0) + { + throw new ArgumentException("Size limit must be greater than zero", nameof(sizeLimit)); + } - #region Transform safe + return Observable.Create>>( + observer => + { + long orderItemWasAdded = -1; + var sizeLimiter = new SizeLimiter(sizeLimit); + + return source.Connect().Finally(observer.OnCompleted).ObserveOn(scheduler ?? Scheduler.Default).Transform((t, v) => new ExpirableItem(t, v, DateTime.Now, Interlocked.Increment(ref orderItemWasAdded))).Select(sizeLimiter.CloneAndReturnExpiredOnly).Where(expired => expired.Length != 0).Subscribe( + toRemove => + { + try + { + source.Remove(toRemove.Select(kv => kv.Key)); + observer.OnNext(toRemove); + } + catch (Exception ex) + { + observer.OnError(ex); + } + }); + }); + } /// - /// Projects each update item to a new form using the specified transform function, - /// providing an error handling action to safely handle transform errors without killing the stream. + /// Dynamically merges the observable which is selected from each item in the stream, and un-merges the item + /// when it is no longer part of the stream. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. + /// The type of the destination. /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// Provides the option to safely handle errors without killing the stream. - /// - /// A transformed update collection - /// + /// The observable selector. + /// An observable which emits the transformed value. /// source /// or - /// transformFactory - public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable> forceTransform = null) + /// observableSelector. + public static IObservable MergeMany(this IObservable> source, Func> observableSelector) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) - { - throw new ArgumentNullException(nameof(transformFactory)); - } - - if (errorHandler == null) + if (observableSelector is null) { - throw new ArgumentNullException(nameof(errorHandler)); + throw new ArgumentNullException(nameof(observableSelector)); } - return source.TransformSafe((current, previous, key) => transformFactory(current), errorHandler, forceTransform.ForForced()); + return new MergeMany(source, observableSelector).Run(); } /// - /// Projects each update item to a new form using the specified transform function, - /// providing an error handling action to safely handle transform errors without killing the stream. + /// Dynamically merges the observable which is selected from each item in the stream, and un-merges the item + /// when it is no longer part of the stream. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. + /// The type of the destination. /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// Provides the option to safely handle errors without killing the stream. - /// - /// A transformed update collection - /// + /// The observable selector. + /// An observable which emits the transformed value. /// source /// or - /// transformFactory - public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable> forceTransform = null) + /// observableSelector. + public static IObservable MergeMany(this IObservable> source, Func> observableSelector) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) - { - throw new ArgumentNullException(nameof(transformFactory)); - } - - if (errorHandler == null) + if (observableSelector is null) { - throw new ArgumentNullException(nameof(errorHandler)); + throw new ArgumentNullException(nameof(observableSelector)); } - return source.TransformSafe((current, previous, key) => transformFactory(current, key), errorHandler, forceTransform); + return new MergeMany(source, observableSelector).Run(); } /// - /// Projects each update item to a new form using the specified transform function, - /// providing an error handling action to safely handle transform errors without killing the stream. + /// Dynamically merges the observable which is selected from each item in the stream, and un-merges the item + /// when it is no longer part of the stream. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. + /// The type of the destination. /// The source. - /// The transform factory. - /// Invoke to force a new transform for items matching the selected objects - /// Provides the option to safely handle errors without killing the stream. - /// - /// A transformed update collection - /// + /// The observable selector. + /// An observable which emits the item with the value. /// source /// or - /// transformFactory - public static IObservable> TransformSafe(this IObservable> source, - Func, TKey, TDestination> transformFactory, - Action> errorHandler, - IObservable> forceTransform = null) + /// observableSelector. + public static IObservable> MergeManyItems(this IObservable> source, Func> observableSelector) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (observableSelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(observableSelector)); } - if (errorHandler == null) + return new MergeManyItems(source, observableSelector).Run(); + } + + /// + /// Dynamically merges the observable which is selected from each item in the stream, and un-merges the item + /// when it is no longer part of the stream. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The source. + /// The observable selector. + /// An observable which emits the item with the value. + /// source + /// or + /// observableSelector. + public static IObservable> MergeManyItems(this IObservable> source, Func> observableSelector) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(errorHandler)); + throw new ArgumentNullException(nameof(source)); } - if (forceTransform != null) + if (observableSelector is null) { - return new TransformWithForcedTransform(source, transformFactory, forceTransform, errorHandler).Run(); + throw new ArgumentNullException(nameof(observableSelector)); } - return new Transform(source, transformFactory, errorHandler).Run(); + return new MergeManyItems(source, observableSelector).Run(); } /// - /// Projects each update item to a new form using the specified transform function, - /// providing an error handling action to safely handle transform errors without killing the stream. + /// Monitors the status of a stream. /// - /// The type of the destination. - /// The type of the source. - /// The type of the key. + /// The type of the source observable. /// The source. - /// The transform factory. - /// Invoke to force a new transform for all items - /// Provides the option to safely handle errors without killing the stream. - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable forceTransform) + /// An observable which monitors the status of the observable. + /// source. + public static IObservable MonitorStatus(this IObservable source) { - return source.TransformSafe((cur, prev, key) => transformFactory(cur), errorHandler, forceTransform.ForForced()); + return new StatusMonitor(source).Run(); } /// - /// Projects each update item to a new form using the specified transform function, - /// providing an error handling action to safely handle transform errors without killing the stream. + /// Suppresses updates which are empty. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. /// The source. - /// The transform factory. - /// Invoke to force a new transform for all items# - /// Provides the option to safely handle errors without killing the stream. - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> TransformSafe(this IObservable> source, - Func transformFactory, Action> errorHandler, IObservable forceTransform) + /// An observable which emits change set values when not empty. + /// source. + public static IObservable> NotEmpty(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + return source.Where(changes => changes.Count != 0); + } + + /// + /// Callback for each item as and when it is being added to the stream. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The add action. + /// An observable which emits a change set with items being added. + public static IObservable> OnItemAdded(this IObservable> source, Action addAction) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(source)); } - if (forceTransform == null) + if (addAction is null) { - throw new ArgumentNullException(nameof(forceTransform)); + throw new ArgumentNullException(nameof(addAction)); } - return source.TransformSafe((cur, prev, key) => transformFactory(cur, key), errorHandler, forceTransform.ForForced()); + return source.Do(changes => changes.Where(c => c.Reason == ChangeReason.Add).ForEach(c => addAction(c.Current))); } /// - /// Projects each update item to a new form using the specified transform function, - /// providing an error handling action to safely handle transform errors without killing the stream. + /// Callback for each item as and when it is being removed from the stream. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. /// The source. - /// The transform factory. - /// Invoke to force a new transform for all items# - /// Provides the option to safely handle errors without killing the stream. - /// - /// A transformed update collection - /// - /// source + /// The remove action. + /// An observable which emits a change set with items being removed. + /// + /// source /// or - /// transformFactory - public static IObservable> TransformSafe(this IObservable> source, - Func, TKey, TDestination> transformFactory, - Action> errorHandler, - IObservable forceTransform) + /// removeAction. + /// + public static IObservable> OnItemRemoved(this IObservable> source, Action removeAction) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) - { - throw new ArgumentNullException(nameof(transformFactory)); - } - - if (forceTransform == null) + if (removeAction is null) { - throw new ArgumentNullException(nameof(forceTransform)); + throw new ArgumentNullException(nameof(removeAction)); } - return source.TransformSafe(transformFactory, errorHandler, forceTransform.ForForced()); + return source.Do(changes => changes.Where(c => c.Reason == ChangeReason.Remove).ForEach(c => removeAction(c.Current))); } - #endregion - - #region Transform safe async - /// - /// Projects each update item to a new form using the specified transform function + /// Callback when an item has been updated eg. (current, previous)=>{}. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. /// The source. - /// The transform factory. - /// The error handler. - /// Invoke to force a new transform for items matching the selected objects - /// The maximum concurrent tasks used to perform transforms. - /// - /// A transformed update collection - /// - /// source - /// or - /// transformFactory - public static IObservable> TransformSafeAsync(this IObservable> source, - Func> transformFactory, - Action> errorHandler, - IObservable> forceTransform = null, - int maximumConcurrency = 1) + /// The update action. + /// An observable which emits a change set with items being updated. + public static IObservable> OnItemUpdated(this IObservable> source, Action updateAction) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) - { - throw new ArgumentNullException(nameof(transformFactory)); - } - - if (errorHandler == null) + if (updateAction is null) { - throw new ArgumentNullException(nameof(errorHandler)); + throw new ArgumentNullException(nameof(updateAction)); } - return source.TransformSafeAsync((current, previous, key) => transformFactory(current), errorHandler, maximumConcurrency, forceTransform); + return source.Do(changes => changes.Where(c => c.Reason == ChangeReason.Update).ForEach(c => updateAction(c.Current, c.Previous.Value))); } /// - /// Projects each update item to a new form using the specified transform function + /// Apply a logical Or operator between the collections i.e items which are in any of the sources are included. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. /// The source. - /// The transform factory. - /// The error handler. - /// Invoke to force a new transform for items matching the selected objects - /// The maximum concurrent tasks used to perform transforms. - /// - /// A transformed update collection - /// - /// source + /// The others. + /// An observable which emits change sets. + /// + /// source /// or - /// transformFactory - public static IObservable> TransformSafeAsync(this IObservable> source, - Func> transformFactory, - Action> errorHandler, - int maximumConcurrency = 1, - IObservable> forceTransform = null) + /// others. + /// + public static IObservable> Or(this IObservable> source, params IObservable>[] others) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) - { - throw new ArgumentNullException(nameof(transformFactory)); - } - - if (errorHandler == null) + if (others is null || others.Length == 0) { - throw new ArgumentNullException(nameof(errorHandler)); + throw new ArgumentNullException(nameof(others)); } - return source.TransformSafeAsync((current, previous, key) => transformFactory(current, key), errorHandler, maximumConcurrency, forceTransform); + return source.Combine(CombineOperator.Or, others); } /// - /// Projects each update item to a new form using the specified transform function + /// Apply a logical Or operator between the collections i.e items which are in any of the sources are included. /// - /// The type of the destination. - /// The type of the source. + /// The type of the object. /// The type of the key. - /// The source. - /// The transform factory. - /// The error handler. - /// Invoke to force a new transform for items matching the selected objects - /// The maximum concurrent tasks used to perform transforms. - /// - /// A transformed update collection - /// - /// source + /// The source. + /// An observable which emits change sets. + /// + /// source /// or - /// transformFactory - public static IObservable> TransformSafeAsync(this IObservable> source, - Func, TKey, Task> transformFactory, - Action> errorHandler, - int maximumConcurrency = 1, - IObservable> forceTransform = null) + /// others. + /// + public static IObservable> Or(this ICollection>> sources) + where TKey : notnull { - if (source == null) + if (sources is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(sources)); } - if (transformFactory == null) - { - throw new ArgumentNullException(nameof(transformFactory)); - } + return sources.Combine(CombineOperator.Or); + } - if (errorHandler == null) + /// + /// Dynamically apply a logical Or operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// An observable which emits change sets. + public static IObservable> Or(this IObservableList>> sources) + where TKey : notnull + { + if (sources is null) { - throw new ArgumentNullException(nameof(errorHandler)); + throw new ArgumentNullException(nameof(sources)); } - return new TransformAsync(source, transformFactory, errorHandler, maximumConcurrency, forceTransform).Run(); + return sources.Combine(CombineOperator.Or); } /// - /// Transforms the object to a fully recursive tree, create a hiearchy based on the pivot function + /// Dynamically apply a logical Or operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The source. - /// The pivot on. - /// Observable to change the underlying predicate. - /// - public static IObservable, TKey>> TransformToTree([NotNull] this IObservable> source, - [NotNull] Func pivotOn, - IObservable, bool>> predicateChanged = null) - where TObject : class + /// The source. + /// An observable which emits change sets. + public static IObservable> Or(this IObservableList> sources) + where TKey : notnull { - if (source == null) + if (sources is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(sources)); } - if (pivotOn == null) + return sources.Combine(CombineOperator.Or); + } + + /// + /// Dynamically apply a logical Or operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// An observable which emits change sets. + public static IObservable> Or(this IObservableList> sources) + where TKey : notnull + { + if (sources is null) { - throw new ArgumentNullException(nameof(pivotOn)); + throw new ArgumentNullException(nameof(sources)); } - return new TreeBuilder(source, pivotOn, predicateChanged).Run(); + return sources.Combine(CombineOperator.Or); } - #endregion - - #region Distinct values - /// - /// Selects distinct values from the source. + /// Returns the page as specified by the pageRequests observable. /// - /// The tyoe object from which the distinct values are selected + /// The type of the object. /// The type of the key. - /// The type of the value. - /// The soure. - /// The value selector. - /// - /// - /// Due to it's nature only adds or removes can be returned - /// - /// source - public static IObservable> DistinctValues(this IObservable> source, Func valueSelector) + /// The source. + /// The page requests. + /// An observable which emits change sets. + public static IObservable> Page(this IObservable> source, IObservable pageRequests) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (pageRequests is null) { - throw new ArgumentNullException(nameof(valueSelector)); + throw new ArgumentNullException(nameof(pageRequests)); } - return Observable.Create>(observer => - { - return new DistinctCalculator(source, valueSelector).Run().SubscribeSafe(observer); - }); + return new Page(source, pageRequests).Run(); } - #endregion - - #region Grouping - /// - /// Groups the source on the value returned by group selector factory. - /// A group is included for each item in the resulting group source. + /// Populate a cache from an observable stream. /// /// The type of the object. /// The type of the key. - /// The type of the group key. /// The source. - /// The group selector factory. - /// - /// A distinct stream used to determine the result - /// - /// - /// Useful for parent-child collection when the parent and child are soured from different streams - /// - /// - public static IObservable> Group( - this IObservable> source, - Func groupSelector, - IObservable> resultGroupSource) + /// The observable. + /// A disposable which will unsubscribe from the source. + /// + /// source + /// or + /// keySelector. + /// + public static IDisposable PopulateFrom(this ISourceCache source, IObservable> observable) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (groupSelector == null) - { - throw new ArgumentNullException(nameof(groupSelector)); - } - - if (resultGroupSource == null) - { - throw new ArgumentNullException(nameof(resultGroupSource)); - } - - return new SpecifiedGrouper(source, groupSelector, resultGroupSource).Run(); + return observable.Subscribe(source.AddOrUpdate); } /// - /// Groups the source on the value returned by group selector factory. + /// Populate a cache from an observable stream. /// /// The type of the object. /// The type of the key. - /// The type of the group key. /// The source. - /// The group selector key. - /// - public static IObservable> Group(this IObservable> source, Func groupSelectorKey) - + /// The observable. + /// A disposable which will unsubscribe from the source. + /// + /// source + /// or + /// keySelector. + /// + public static IDisposable PopulateFrom(this ISourceCache source, IObservable observable) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (groupSelectorKey == null) - { - throw new ArgumentNullException(nameof(groupSelectorKey)); - } - - return new GroupOn(source, groupSelectorKey, null).Run(); + return observable.Subscribe(source.AddOrUpdate); } /// - /// Groups the source on the value returned by group selector factory. + /// Populates a source into the specified cache. /// /// The type of the object. /// The type of the key. - /// The type of the group key. /// The source. - /// The group selector key. - /// Invoke to the for the grouping to be re-evaluated - /// + /// The destination. + /// A disposable which will unsubscribe from the source. /// /// source /// or - /// groupSelectorKey - /// or - /// groupController + /// destination. /// - public static IObservable> Group(this IObservable> source, - Func groupSelectorKey, - IObservable regrouper) + public static IDisposable PopulateInto(this IObservable> source, ISourceCache destination) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (groupSelectorKey == null) - { - throw new ArgumentNullException(nameof(groupSelectorKey)); - } - - if (regrouper == null) + if (destination is null) { - throw new ArgumentNullException(nameof(regrouper)); + throw new ArgumentNullException(nameof(destination)); } - return new GroupOn(source, groupSelectorKey, regrouper).Run(); + return source.Subscribe(changes => destination.Edit(updater => updater.Clone(changes))); } /// - /// Groups the source on the value returned by group selector factory. Each update produces immuatable grouping. + /// Populates a source into the specified cache. /// /// The type of the object. /// The type of the key. - /// The type of the group key. /// The source. - /// The group selector key. - /// Invoke to the for the grouping to be re-evaluated - /// - /// - /// source - /// or - /// groupSelectorKey + /// The destination. + /// A disposable which will unsubscribe from the source. + /// source /// or - /// groupController - /// - public static IObservable> GroupWithImmutableState(this IObservable> source, - Func groupSelectorKey, - IObservable regrouper = null) + /// destination. + public static IDisposable PopulateInto(this IObservable> source, IIntermediateCache destination) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (groupSelectorKey == null) + if (destination is null) { - throw new ArgumentNullException(nameof(groupSelectorKey)); + throw new ArgumentNullException(nameof(destination)); } - return new GroupOnImmutable(source, groupSelectorKey, regrouper).Run(); + return source.Subscribe(changes => destination.Edit(updater => updater.Clone(changes))); } /// - /// Groups the source using the property specified by the property selector. Groups are re-applied when the property value changed. - /// - /// When there are likely to be a large number of group property changes specify a throttle to improve performance + /// Populates a source into the specified cache. /// /// The type of the object. /// The type of the key. - /// The type of the group key. /// The source. - /// The property selector used to group the items - /// - /// The scheduler. - /// - /// - /// - public static IObservable> GroupOnProperty(this IObservable> source, - Expression> propertySelector, - TimeSpan? propertyChangedThrottle = null, - IScheduler scheduler = null) - where TObject : INotifyPropertyChanged + /// The destination. + /// A disposable which will unsubscribe from the source. + public static IDisposable PopulateInto(this IObservable> source, LockFreeObservableCache destination) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertySelector == null) + if (destination is null) { - throw new ArgumentNullException(nameof(propertySelector)); + throw new ArgumentNullException(nameof(destination)); } - return new GroupOnProperty(source, propertySelector, propertyChangedThrottle, scheduler).Run(); + return source.Subscribe(changes => destination.Edit(updater => updater.Clone(changes))); } /// - /// Groups the source using the property specified by the property selector. Each update produces immuatable grouping. Groups are re-applied when the property value changed. - /// - /// When there are likely to be a large number of group property changes specify a throttle to improve performance + /// The latest copy of the cache is exposed for querying after each modification to the underlying data. /// /// The type of the object. /// The type of the key. - /// The type of the group key. + /// The type of the destination. /// The source. - /// The property selector used to group the items - /// - /// The scheduler. - /// + /// The result selector. + /// An observable which emits the destination values. /// + /// source + /// or + /// resultSelector. /// - public static IObservable> GroupOnPropertyWithImmutableState(this IObservable> source, - Expression> propertySelector, - TimeSpan? propertyChangedThrottle = null, - IScheduler scheduler = null) - where TObject : INotifyPropertyChanged + public static IObservable QueryWhenChanged(this IObservable> source, Func, TDestination> resultSelector) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertySelector == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(propertySelector)); + throw new ArgumentNullException(nameof(resultSelector)); } - return new GroupOnPropertyWithImmutableState(source, propertySelector, propertyChangedThrottle, scheduler).Run(); + return source.QueryWhenChanged().Select(resultSelector); } - #endregion - - #region Virtualisation - /// - /// Limits the size of the result set to the specified number + /// The latest copy of the cache is exposed for querying i) after each modification to the underlying data ii) upon subscription. /// /// The type of the object. /// The type of the key. /// The source. - /// The size. - /// - /// source - /// size;Size should be greater than zero - public static IObservable> Top( - this IObservable> source, int size) + /// An observable which emits the query. + /// source. + public static IObservable> QueryWhenChanged(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (size <= 0) - { - throw new ArgumentOutOfRangeException(nameof(size), "Size should be greater than zero"); - } - - return new Virtualise(source, Observable.Return(new VirtualRequest(0, size))).Run(); + return new QueryWhenChanged(source).Run(); } /// - /// Limits the size of the result set to the specified number, ordering by the comparer + /// The latest copy of the cache is exposed for querying i) after each modification to the underlying data ii) on subscription. /// /// The type of the object. /// The type of the key. + /// The type of the value. /// The source. - /// The comparer. - /// The size. - /// - /// source - /// size;Size should be greater than zero - public static IObservable> Top( - this IObservable> source, - IComparer comparer, - int size) + /// Should the query be triggered for observables on individual items. + /// An observable that emits the query. + /// source. + public static IObservable> QueryWhenChanged(this IObservable> source, Func> itemChangedTrigger) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (comparer == null) - { - throw new ArgumentNullException(nameof(comparer)); - } - - if (size <= 0) + if (itemChangedTrigger is null) { - throw new ArgumentOutOfRangeException(nameof(size), "Size should be greater than zero"); + throw new ArgumentNullException(nameof(itemChangedTrigger)); } - return source.Sort(comparer).Top(size); + return new QueryWhenChanged(source, itemChangedTrigger).Run(); } /// - /// Virtualises the underlying data from the specified source. + /// Cache equivalent to Publish().RefCount(). The source is cached so long as there is at least 1 subscriber. /// /// The type of the object. - /// The type of the key. + /// The type of the destination key. /// The source. - /// The virirtualising requests - /// - /// source - public static IObservable> Virtualise(this IObservable> source, - IObservable virtualRequests) + /// An observable which emits change sets that are ref counted. + public static IObservable> RefCount(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (virtualRequests == null) - { - throw new ArgumentNullException(nameof(virtualRequests)); - } - - return new Virtualise(source, virtualRequests).Run(); + return new RefCount(source).Run(); } - #endregion - - #region Binding - /// - /// Binds the results to the specified observable collection collection using the default update algorithm + /// Signal observers to re-evaluate the specified item. /// /// The type of the object. /// The type of the key. /// The source. - /// The destination. - /// - /// source - public static IObservable> Bind(this IObservable> source, - IObservableCollection destination) + /// The item. + /// source. + public static void Refresh(this ISourceCache source, TObject item) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } - - var updater = new ObservableCollectionAdaptor(); - return source.Bind(destination, updater); + source.Edit(updater => updater.Refresh(item)); } /// - /// Binds the results to the specified binding collection using the specified update algorithm + /// Signal observers to re-evaluate the specified items. /// /// The type of the object. /// The type of the key. /// The source. - /// The destination. - /// The updater. - /// - /// source - public static IObservable> Bind(this IObservable> source, - IObservableCollection destination, - IObservableCollectionAdaptor updater) + /// The items. + /// source. + public static void Refresh(this ISourceCache source, IEnumerable items) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } + source.Edit(updater => updater.Refresh(items)); + } - if (updater == null) + /// + /// Signal observers to re-evaluate the all items. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// source. + public static void Refresh(this ISourceCache source) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(updater)); + throw new ArgumentNullException(nameof(source)); } - return Observable.Create>(observer => - { - var locker = new object(); - return source - .Synchronize(locker) - .Select(changes => - { - updater.Adapt(changes, destination); - return changes; - }).SubscribeSafe(observer); - }); + source.Edit(updater => updater.Refresh()); } /// - /// Binds the results to the specified observable collection collection using the default update algorithm + /// Removes the specified item from the cache. + /// + /// If the item is not contained in the cache then the operation does nothing. /// /// The type of the object. /// The type of the key. /// The source. - /// The destination. - /// - /// source - public static IObservable> Bind(this IObservable> source, - IObservableCollection destination) + /// The item. + /// source. + public static void Remove(this ISourceCache source, TObject item) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } - - var updater = new SortedObservableCollectionAdaptor(); - return source.Bind(destination, updater); + source.Edit(updater => updater.Remove(item)); } /// - /// Binds the results to the specified binding collection using the specified update algorithm + /// Removes the specified key from the cache. + /// If the item is not contained in the cache then the operation does nothing. /// /// The type of the object. /// The type of the key. /// The source. - /// The destination. - /// The updater. - /// - /// source - public static IObservable> Bind( - this IObservable> source, - IObservableCollection destination, - ISortedObservableCollectionAdaptor updater) + /// The key. + /// source. + public static void Remove(this ISourceCache source, TKey key) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } + source.Edit(updater => updater.Remove(key)); + } - if (updater == null) + /// + /// Removes the specified items from the cache. + /// + /// Any items not contained in the cache are ignored. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The items. + /// source. + public static void Remove(this ISourceCache source, IEnumerable items) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(updater)); + throw new ArgumentNullException(nameof(source)); } - return Observable.Create>(observer => - { - var locker = new object(); - return source - .Synchronize(locker) - .Select(changes => - { - updater.Adapt(changes, destination); - return changes; - }).SubscribeSafe(observer); - }); + source.Edit(updater => updater.Remove(items)); } /// - /// Binds the results to the specified readonly observable collection collection using the default update algorithm + /// Removes the specified keys from the cache. + /// + /// Any keys not contained in the cache are ignored. /// /// The type of the object. /// 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 - /// Specify an adaptor to change the algorithm to update the target collection - /// - /// source - public static IObservable> Bind(this IObservable> source, - out ReadOnlyObservableCollection readOnlyObservableCollection, - int resetThreshold = 25, - ISortedObservableCollectionAdaptor adaptor = null) + /// The keys. + /// source. + public static void Remove(this ISourceCache source, IEnumerable keys) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - var target = new ObservableCollectionExtended(); - var result = new ReadOnlyObservableCollection(target); - var updater = adaptor ?? new SortedObservableCollectionAdaptor(resetThreshold); - readOnlyObservableCollection = result; - return source.Bind(target, updater); + source.Edit(updater => updater.Remove(keys)); } /// - /// Binds the results to the specified readonly observable collection collection using the default update algorithm + /// Removes the specified key from the cache. + /// If the item is not contained in the cache then the operation does nothing. /// /// The type of the object. /// 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 - /// Specify an adaptor to change the algorithm to update the target collection - /// - /// source - public static IObservable> Bind(this IObservable> source, - out ReadOnlyObservableCollection readOnlyObservableCollection, - int resetThreshold = 25, - IObservableCollectionAdaptor adaptor = null) + /// The key. + /// source. + public static void Remove(this IIntermediateCache source, TKey key) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - var target = new ObservableCollectionExtended(); - var result = new ReadOnlyObservableCollection(target); - var updater = adaptor ?? new ObservableCollectionAdaptor(resetThreshold); - readOnlyObservableCollection = result; - return source.Bind(target, updater); + source.Edit(updater => updater.Remove(key)); } -#if SUPPORTS_BINDINGLIST - /// - /// Binds a clone of the observable changeset to the target observable collection + /// Removes the specified keys from the cache. + /// + /// Any keys not contained in the cache are ignored. /// - /// The object type - /// The key type - /// The source. - /// The target binding list - /// The reset threshold. - /// - /// source - /// or - /// targetCollection - /// - public static IObservable> Bind([NotNull] this IObservable> source, - [NotNull] BindingList bindingList, int resetThreshold = 25) + /// The type of the object. + /// The type of the key. + /// The source. + /// The keys. + /// source. + public static void Remove(this IIntermediateCache source, IEnumerable keys) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (bindingList == null) - { - throw new ArgumentNullException(nameof(bindingList)); - } - - return source.Adapt(new BindingListAdaptor(bindingList, resetThreshold)); + source.Edit(updater => updater.Remove(keys)); } /// - /// Binds a clone of the observable changeset to the target observable collection + /// Removes the key which enables all observable list features of dynamic data. /// - /// The object type - /// The key type + /// + /// All indexed changes are dropped i.e. sorting is not supported by this function. + /// + /// The type of object. + /// The type of key. /// The source. - /// The target binding list - /// The reset threshold. - /// - /// source - /// or - /// targetCollection - /// - public static IObservable> Bind([NotNull] this IObservable> source, - [NotNull] BindingList bindingList, int resetThreshold = 25) + /// An observable which emits change sets. + public static IObservable> RemoveKey(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (bindingList == null) - { - throw new ArgumentNullException(nameof(bindingList)); - } - - return source.Adapt(new SortedBindingListAdaptor(bindingList, resetThreshold)); + return source.Select( + changes => + { + var enumerator = new RemoveKeyEnumerator(changes); + return new ChangeSet(enumerator); + }); } -#endif - - #endregion - - #region Adaptor - /// - /// Inject side effects into the stream using the specified adaptor + /// Removes the specified key from the cache. + /// If the item is not contained in the cache then the operation does nothing. /// /// The type of the object. /// The type of the key. /// The source. - /// The adaptor. - /// - /// - /// source - /// or - /// destination - /// - public static IObservable> Adapt(this IObservable> source, IChangeSetAdaptor adaptor) + /// The key. + /// source. + public static void RemoveKey(this ISourceCache source, TKey key) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (adaptor == null) - { - throw new ArgumentNullException(nameof(adaptor)); - } - - return source.Do(adaptor.Adapt); + source.Edit(updater => updater.RemoveKey(key)); } /// - /// Inject side effects into the stream using the specified sorted adaptor + /// Removes the specified keys from the cache. + /// Any keys not contained in the cache are ignored. /// /// The type of the object. /// The type of the key. /// The source. - /// The adaptor. - /// - /// - /// source - /// or - /// destination - /// - public static IObservable> Adapt(this IObservable> source, ISortedChangeSetAdaptor adaptor) + /// The keys. + /// source. + public static void RemoveKeys(this ISourceCache source, IEnumerable keys) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (adaptor == null) - { - throw new ArgumentNullException(nameof(adaptor)); - } - - return source.Do(adaptor.Adapt); + source.Edit(updater => updater.RemoveKeys(keys)); } - #endregion - - #region Joins /// - /// Joins the left and right observable data sources, taking values when both left and right values are present - /// This is the equivalent of SQL inner join. + /// Joins the left and right observable data sources, taking all right values and combining any matching left values. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> InnerJoin(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func resultSelector) + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> RightJoin(this IObservable> left, IObservable> right, Func rightKeySelector, Func, TRight, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (left == null) + if (left is null) { throw new ArgumentNullException(nameof(left)); } - if (right == null) + if (right is null) { throw new ArgumentNullException(nameof(right)); } - if (rightKeySelector == null) + if (rightKeySelector is null) { throw new ArgumentNullException(nameof(rightKeySelector)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return left.InnerJoin(right, rightKeySelector, (leftKey, leftValue, rightValue) => resultSelector(leftValue, rightValue)); + return left.RightJoin(right, rightKeySelector, (_, leftValue, rightValue) => resultSelector(leftValue, rightValue)); } /// - /// Groups the right data source and joins the to the left and the right sources, taking values when both left and right values are present - /// This is the equivalent of SQL inner join. + /// Joins the left and right observable data sources, taking all right values and combining any matching left values. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> InnerJoin(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func resultSelector) + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> RightJoin(this IObservable> left, IObservable> right, Func rightKeySelector, Func, TRight, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (left == null) + if (left is null) { throw new ArgumentNullException(nameof(left)); } - if (right == null) + if (right is null) { throw new ArgumentNullException(nameof(right)); } - if (rightKeySelector == null) + if (rightKeySelector is null) { throw new ArgumentNullException(nameof(rightKeySelector)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return new InnerJoin(left, right, rightKeySelector, resultSelector).Run(); + return new RightJoin(left, right, rightKeySelector, resultSelector).Run(); } /// - /// Groups the right data source and joins the resulting group to the left data source, matching these using the specified key selector. Results are included when the left and right have matching values. - /// This is the equivalent of SQL inner join. + /// Groups the right data source and joins the two sources matching them using the specified key selector, , taking all right values and combining any matching left values. + /// This is the equivalent of SQL left join. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> InnerJoinMany(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, TDestination> resultSelector) + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> RightJoinMany(this IObservable> left, IObservable> right, Func rightKeySelector, Func, IGrouping, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (left == null) + if (left is null) { throw new ArgumentNullException(nameof(left)); } - if (right == null) + if (right is null) { throw new ArgumentNullException(nameof(right)); } - if (rightKeySelector == null) + if (rightKeySelector is null) { throw new ArgumentNullException(nameof(rightKeySelector)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return left.InnerJoinMany(right, rightKeySelector, (leftKey, leftValue, rightValue) => resultSelector(leftValue, rightValue)); + return left.RightJoinMany(right, rightKeySelector, (_, leftValue, rightValue) => resultSelector(leftValue, rightValue)); } /// - /// Groups the right data source and joins the resulting group to the left data source, matching these using the specified key selector. Results are included when the left and right have matching values. - /// This is the equivalent of SQL inner join. + /// Groups the right data source and joins the two sources matching them using the specified key selector,, taking all right values and combining any matching left values. + /// This is the equivalent of SQL left join. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source + /// The object type of the left data source. + /// The key type of the left data source. + /// The object type of the right data source. + /// The key type of the right data source. + /// The resulting object which. + /// The left data source. /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> InnerJoinMany(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, TDestination> resultSelector) + /// Specify the foreign key on the right data source. + /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right). + /// An observable which will emit change sets. + public static IObservable> RightJoinMany(this IObservable> left, IObservable> right, Func rightKeySelector, Func, IGrouping, TDestination> resultSelector) + where TLeftKey : notnull + where TRightKey : notnull { - if (left == null) + if (left is null) { throw new ArgumentNullException(nameof(left)); } - if (right == null) + if (right is null) { throw new ArgumentNullException(nameof(right)); } - if (rightKeySelector == null) + if (rightKeySelector is null) { throw new ArgumentNullException(nameof(rightKeySelector)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } - return new InnerJoinMany(left, right, rightKeySelector, resultSelector).Run(); + return new RightJoinMany(left, right, rightKeySelector, resultSelector).Run(); } /// - /// Joins the left and right observable data sources, taking any left or right values and matching them, provided that the left or the right has a value. - /// This is the equivalent of SQL full join. + /// Defer the subscription until loaded and skip initial change set. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> FullJoin(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, Optional, TDestination> resultSelector) + /// The type of the object. + /// The type of the key. + /// The source. + /// An observable which emits change sets. + /// source. + public static IObservable> SkipInitial(this IObservable> source) + where TKey : notnull { - if (left == null) + if (source is null) { - throw new ArgumentNullException(nameof(left)); + throw new ArgumentNullException(nameof(source)); + } + + return source.DeferUntilLoaded().Skip(1); + } + + /// + /// Sorts using the specified comparer. + /// Returns the underlying ChangeSet as as per the system conventions. + /// The resulting change set also exposes a sorted key value collection of of the underlying cached data. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The comparer. + /// Sort optimisation flags. Specify one or more sort optimisations. + /// The number of updates before the entire list is resorted (rather than inline sort). + /// An observable which emits change sets. + /// + /// source + /// or + /// comparer. + /// + public static IObservable> Sort(this IObservable> source, IComparer comparer, SortOptimisations sortOptimisations = SortOptimisations.None, int resetThreshold = DefaultSortResetThreshold) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); } - if (right == null) + if (comparer is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(comparer)); } - if (rightKeySelector == null) + return new Sort(source, comparer, sortOptimisations, resetThreshold: resetThreshold).Run(); + } + + /// + /// Sorts a sequence as, using the comparer observable to determine order. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The comparer observable. + /// The sort optimisations. + /// The reset threshold. + /// An observable which emits change sets. + public static IObservable> Sort(this IObservable> source, IObservable> comparerObservable, SortOptimisations sortOptimisations = SortOptimisations.None, int resetThreshold = DefaultSortResetThreshold) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(source)); } - if (resultSelector == null) + if (comparerObservable is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(comparerObservable)); } - return left.FullJoin(right, rightKeySelector, (leftKey, leftValue, rightValue) => resultSelector(leftValue, rightValue)); + return new Sort(source, null, sortOptimisations, comparerObservable, resetThreshold: resetThreshold).Run(); } /// - /// Joins the left and right observable data sources, taking any left or right values and matching them, provided that the left or the right has a value. - /// This is the equivalent of SQL full join. + /// Sorts a sequence as, using the comparer observable to determine order. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> FullJoin(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, Optional, TDestination> resultSelector) + /// The type of the object. + /// The type of the key. + /// The source. + /// The comparer observable. + /// Signal to instruct the algorithm to re-sort the entire data set. + /// The sort optimisations. + /// The reset threshold. + /// An observable which emits change sets. + public static IObservable> Sort(this IObservable> source, IObservable> comparerObservable, IObservable resorter, SortOptimisations sortOptimisations = SortOptimisations.None, int resetThreshold = DefaultSortResetThreshold) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (comparerObservable is null) + { + throw new ArgumentNullException(nameof(comparerObservable)); + } + + return new Sort(source, null, sortOptimisations, comparerObservable, resorter, resetThreshold).Run(); + } + + /// + /// Sorts a sequence as, using the comparer observable to determine order. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The comparer to sort on. + /// Signal to instruct the algorithm to re-sort the entire data set. + /// The sort optimisations. + /// The reset threshold. + /// An observable which emits change sets. + public static IObservable> Sort(this IObservable> source, IComparer comparer, IObservable resorter, SortOptimisations sortOptimisations = SortOptimisations.None, int resetThreshold = DefaultSortResetThreshold) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (resorter is null) + { + throw new ArgumentNullException(nameof(resorter)); + } + + return new Sort(source, comparer, sortOptimisations, null, resorter, resetThreshold).Run(); + } + + /// + /// Prepends an empty change set to the source. + /// + /// The object of the change set. + /// The key of the change set. + /// The source observable change set. + /// An observable which emits change sets. + public static IObservable> StartWithEmpty(this IObservable> source) + where TKey : notnull + { + return source.StartWith(ChangeSet.Empty); + } + + /// + /// Prepends an empty change set to the source. + /// + /// The object of the change set. + /// The key of the change set. + /// The source observable change set. + /// An observable which emits sorted change sets. + public static IObservable> StartWithEmpty(this IObservable> source) + where TKey : notnull + { + return source.StartWith(SortedChangeSet.Empty); + } + + /// + /// Prepends an empty change set to the source. + /// + /// The object of the change set. + /// The key of the change set. + /// The source observable change set. + /// An observable which emits virtual change sets. + public static IObservable> StartWithEmpty(this IObservable> source) + where TKey : notnull + { + return source.StartWith(VirtualChangeSet.Empty); + } + + /// + /// Prepends an empty change set to the source. + /// + /// The object of the change set. + /// The key of the change set. + /// The source observable change set. + /// An observable which emits paged change sets. + public static IObservable> StartWithEmpty(this IObservable> source) + where TKey : notnull + { + return source.StartWith(PagedChangeSet.Empty); + } + + /// + /// Prepends an empty change set to the source. + /// + /// The object of the change set. + /// The key of the change set. + /// The grouping key type. + /// The source observable change set. + /// An observable which emits group change sets. + public static IObservable> StartWithEmpty(this IObservable> source) + where TKey : notnull + where TGroupKey : notnull + { + return source.StartWith(GroupChangeSet.Empty); + } + + /// + /// Prepends an empty change set to the source. + /// + /// The object of the change set. + /// The key of the change set. + /// The grouping key type. + /// The source observable change set. + /// An observable which emits immutable group change sets. + public static IObservable> StartWithEmpty(this IObservable> source) + where TKey : notnull + where TGroupKey : notnull + { + return source.StartWith(ImmutableGroupChangeSet.Empty); + } + + /// + /// Prepends an empty change set to the source. + /// + /// The type of the item. + /// The source read only collection. + /// A read only collection. + public static IObservable> StartWithEmpty(this IObservable> source) + { + return source.StartWith(ReadOnlyCollectionLight.Empty); + } + + /// + /// The equivalent of rx StartsWith operator, but wraps the item in a change where reason is ChangeReason.Add. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The item. + /// An observable which emits change sets. + public static IObservable> StartWithItem(this IObservable> source, TObject item) + where TObject : IKey + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return source.StartWithItem(item, item.Key); + } + + /// + /// The equivalent of rx StartWith operator, but wraps the item in a change where reason is ChangeReason.Add. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The item. + /// The key. + /// An observable which emits change sets. + public static IObservable> StartWithItem(this IObservable> source, TObject item, TKey key) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + var change = new Change(ChangeReason.Add, key, item); + return source.StartWith(new ChangeSet { change }); + } + + /// + /// Subscribes to each item when it is added to the stream and un-subscribes when it is removed. All items will be unsubscribed when the stream is disposed. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The subscription function. + /// An observable which emits a change set. + /// source + /// or + /// subscriptionFactory. + /// + /// Subscribes to each item when it is added or updates and un-subscribes when it is removed. + /// + public static IObservable> SubscribeMany(this IObservable> source, Func subscriptionFactory) + where TKey : notnull { - if (left == null) + if (source is null) { - throw new ArgumentNullException(nameof(left)); + throw new ArgumentNullException(nameof(source)); } - if (right == null) + if (subscriptionFactory is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(subscriptionFactory)); } - if (rightKeySelector == null) + return new SubscribeMany(source, subscriptionFactory).Run(); + } + + /// + /// Subscribes to each item when it is added to the stream and unsubscribes when it is removed. All items will be unsubscribed when the stream is disposed. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The subscription function. + /// An observable which emits a change set. + /// source + /// or + /// subscriptionFactory. + /// + /// Subscribes to each item when it is added or updates and unsubscribes when it is removed. + /// + public static IObservable> SubscribeMany(this IObservable> source, Func subscriptionFactory) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(source)); } - if (resultSelector == null) + if (subscriptionFactory is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(subscriptionFactory)); } - return new FullJoin(left, right, rightKeySelector, resultSelector).Run(); + return new SubscribeMany(source, subscriptionFactory).Run(); } /// - /// Groups the right data source and joins the resulting group to the left data source, matching these using the specified key selector. Results are included when the left or the right has a value. - /// This is the equivalent of SQL full join. + /// Suppress refresh notifications. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> FullJoinMany(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, IGrouping, TDestination> resultSelector) + /// The object of the change set. + /// The key of the change set. + /// The source observable change set. + /// An observable which emits change sets. + public static IObservable> SuppressRefresh(this IObservable> source) + where TKey : notnull { - if (left == null) - { - throw new ArgumentNullException(nameof(left)); - } + return source.WhereReasonsAreNot(ChangeReason.Refresh); + } - if (right == null) + /// + /// Transforms an observable sequence of observable caches into a single sequence + /// producing values only from the most recent observable sequence. + /// Each time a new inner observable sequence is received, unsubscribe from the + /// previous inner observable sequence and clear the existing result set. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// + /// The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received. + /// + public static IObservable> Switch(this IObservable> sources) + where TKey : notnull + { + if (sources is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(sources)); } - if (rightKeySelector == null) - { - throw new ArgumentNullException(nameof(rightKeySelector)); - } + return sources.Select(cache => cache.Connect()).Switch(); + } - if (resultSelector == null) + /// + /// Transforms an observable sequence of observable changes sets into an observable sequence + /// producing values only from the most recent observable sequence. + /// Each time a new inner observable sequence is received, unsubscribe from the + /// previous inner observable sequence and clear the existing result set. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// + /// The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received. + /// + public static IObservable> Switch(this IObservable>> sources) + where TKey : notnull + { + if (sources is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(sources)); } - return left.FullJoinMany(right, rightKeySelector, (leftKey, leftValue, rightValue) => resultSelector(leftValue, rightValue)); + return new Switch(sources).Run(); } /// - /// Groups the right data source and joins the resulting group to the left data source, matching these using the specified key selector. Results are included when the left or the right has a value. - /// This is the equivalent of SQL full join. + /// Converts the change set into a fully formed collection. Each change in the source results in a new collection. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> FullJoinMany(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, IGrouping, TDestination> resultSelector) + /// The type of the object. + /// The type of the key. + /// The source. + /// An observable which emits the read only collection. + public static IObservable> ToCollection(this IObservable> source) + where TKey : notnull { - if (left == null) - { - throw new ArgumentNullException(nameof(left)); - } - - if (right == null) - { - throw new ArgumentNullException(nameof(right)); - } + return source.QueryWhenChanged(query => new ReadOnlyCollectionLight(query.Items)); + } - if (rightKeySelector == null) + /// + /// Converts the observable to an observable change set. + /// Change set observes observable change events. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The key selector. + /// Specify on a per object level the maximum time before an object expires from a cache. + /// Remove the oldest items when the size has reached this limit. + /// The scheduler (only used for time expiry). + /// An observable which will emit changes. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable source, Func keySelector, Func? expireAfter = null, int limitSizeTo = -1, IScheduler? scheduler = null) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(source)); } - if (resultSelector == null) + if (keySelector is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(keySelector)); } - return new FullJoinMany(left, right, rightKeySelector, resultSelector).Run(); + return new ToObservableChangeSet(source, keySelector, expireAfter, limitSizeTo, scheduler).Run(); } /// - /// Joins the left and right observable data sources, taking all left values and combining any matching right values. + /// Converts the observable to an observable change set. + /// Change set observes observable change events. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> LeftJoin(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, TDestination> resultSelector) + /// The type of the object. + /// The type of the key. + /// The source. + /// The key selector. + /// Specify on a per object level the maximum time before an object expires from a cache. + /// Remove the oldest items when the size has reached this limit. + /// The scheduler (only used for time expiry). + /// An observable change set. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable> source, Func keySelector, Func? expireAfter = null, int limitSizeTo = -1, IScheduler? scheduler = null) + where TKey : notnull { - if (left == null) + if (source is null) { - throw new ArgumentNullException(nameof(left)); + throw new ArgumentNullException(nameof(source)); } - if (right == null) + if (keySelector is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(keySelector)); } - if (rightKeySelector == null) + return new ToObservableChangeSet(source, keySelector, expireAfter, limitSizeTo, scheduler).Run(); + } + + /// + /// Limits the size of the result set to the specified number. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The size. + /// An observable which will emit virtual change sets. + /// source. + /// size;Size should be greater than zero. + public static IObservable> Top(this IObservable> source, int size) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(source)); } - if (resultSelector == null) + if (size <= 0) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentOutOfRangeException(nameof(size), "Size should be greater than zero"); } - return left.LeftJoin(right, rightKeySelector, (leftKey, leftValue, rightValue) => resultSelector(leftValue, rightValue)); + return new Virtualise(source, Observable.Return(new VirtualRequest(0, size))).Run(); } /// - /// Joins the left and right observable data sources, taking all left values and combining any matching right values. + /// Limits the size of the result set to the specified number, ordering by the comparer. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> LeftJoin(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, TDestination> resultSelector) + /// The type of the object. + /// The type of the key. + /// The source. + /// The comparer. + /// The size. + /// An observable which will emit virtual change sets. + /// source. + /// size;Size should be greater than zero. + public static IObservable> Top(this IObservable> source, IComparer comparer, int size) + where TKey : notnull { - if (left == null) - { - throw new ArgumentNullException(nameof(left)); - } - - if (right == null) + if (source is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(source)); } - if (rightKeySelector == null) + if (comparer is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(comparer)); } - if (resultSelector == null) + if (size <= 0) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentOutOfRangeException(nameof(size), "Size should be greater than zero"); } - return new LeftJoin(left, right, rightKeySelector, resultSelector).Run(); + return source.Sort(comparer).Top(size); } /// - /// Groups the right data source and joins the two sources matching them using the specified key selector, taking all left values and combining any matching right values. - /// This is the equivalent of SQL left join. + /// Converts the change set into a fully formed sorted collection. Each change in the source results in a new sorted collection. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> LeftJoinMany(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, TDestination> resultSelector) + /// The type of the object. + /// The type of the key. + /// The sort key. + /// The source. + /// The sort function. + /// The sort order. Defaults to ascending. + /// An observable which emits the read only collection. + public static IObservable> ToSortedCollection(this IObservable> source, Func sort, SortDirection sortOrder = SortDirection.Ascending) + where TKey : notnull + where TSortKey : notnull { - if (left == null) - { - throw new ArgumentNullException(nameof(left)); - } + return source.QueryWhenChanged(query => sortOrder == SortDirection.Ascending ? new ReadOnlyCollectionLight(query.Items.OrderBy(sort)) : new ReadOnlyCollectionLight(query.Items.OrderByDescending(sort))); + } - if (right == null) - { - throw new ArgumentNullException(nameof(right)); - } + /// + /// Converts the change set into a fully formed sorted collection. Each change in the source results in a new sorted collection. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The sort comparer. + /// An observable which emits the read only collection. + public static IObservable> ToSortedCollection(this IObservable> source, IComparer comparer) + where TKey : notnull + { + return source.QueryWhenChanged( + query => + { + var items = query.Items.AsList(); + items.Sort(comparer); + return new ReadOnlyCollectionLight(items); + }); + } - if (rightKeySelector == null) + /// + /// Projects each update item to a new form using the specified transform function. + /// + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Should a new transform be applied when a refresh event is received. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func transformFactory, bool transformOnRefresh) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(source)); } - if (resultSelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - return left.LeftJoinMany(right, rightKeySelector, (leftKey, leftValue, rightValue) => resultSelector(leftValue, rightValue)); + return source.Transform((current, _, _) => transformFactory(current), transformOnRefresh); } /// - /// Groups the right data source and joins the two sources matching them using the specified key selector, taking all left values and combining any matching right values. - /// This is the equivalent of SQL left join. + /// Projects each update item to a new form using the specified transform function. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> LeftJoinMany(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, TDestination> resultSelector) + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Should a new transform be applied when a refresh event is received. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func transformFactory, bool transformOnRefresh) + where TKey : notnull { - if (left == null) + if (source is null) { - throw new ArgumentNullException(nameof(left)); + throw new ArgumentNullException(nameof(source)); } - if (right == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(transformFactory)); } - if (rightKeySelector == null) + return source.Transform((current, _, key) => transformFactory(current, key), transformOnRefresh); + } + + /// + /// Projects each update item to a new form using the specified transform function. + /// + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Should a new transform be applied when a refresh event is received. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func, TKey, TDestination> transformFactory, bool transformOnRefresh) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(source)); } - if (resultSelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - return new LeftJoinMany(left, right, rightKeySelector, resultSelector).Run(); + return new Transform(source, transformFactory, transformOnRefresh: transformOnRefresh).Run(); } /// - /// Joins the left and right observable data sources, taking all right values and combining any matching left values. + /// Projects each update item to a new form using the specified transform function. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> RightJoin(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, TRight, TDestination> resultSelector) + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func transformFactory, IObservable>? forceTransform = null) + where TKey : notnull { - if (left == null) + if (source is null) { - throw new ArgumentNullException(nameof(left)); + throw new ArgumentNullException(nameof(source)); } - if (right == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(transformFactory)); } - if (rightKeySelector == null) + return source.Transform((current, _, _) => transformFactory(current), forceTransform?.ForForced()); + } + + /// + /// Projects each update item to a new form using the specified transform function. + /// + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func transformFactory, IObservable>? forceTransform = null) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(source)); } - if (resultSelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - return left.RightJoin(right, rightKeySelector, (leftKey, leftValue, rightValue) => resultSelector(leftValue, rightValue)); + return source.Transform((current, _, key) => transformFactory(current, key), forceTransform); } /// - /// Joins the left and right observable data sources, taking all right values and combining any matching left values. + /// Projects each update item to a new form using the specified transform function. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> RightJoin(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, TRight, TDestination> resultSelector) + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func, TKey, TDestination> transformFactory, IObservable>? forceTransform = null) + where TKey : notnull { - if (left == null) - { - throw new ArgumentNullException(nameof(left)); - } - - if (right == null) + if (source is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(source)); } - if (rightKeySelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - if (resultSelector == null) + if (forceTransform is not null) { - throw new ArgumentNullException(nameof(resultSelector)); + return new TransformWithForcedTransform(source, transformFactory, forceTransform).Run(); } - return new RightJoin(left, right, rightKeySelector, resultSelector).Run(); + return new Transform(source, transformFactory).Run(); } /// - /// Groups the right data source and joins the two sources matching them using the specified key selector, , taking all right values and combining any matching left values. - /// This is the equivalent of SQL left join. + /// Projects each update item to a new form using the specified transform function. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (left, right) => new CustomObject(key, left, right) - /// - /// - /// - public static IObservable> RightJoinMany(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, IGrouping, TDestination> resultSelector) + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Invoke to force a new transform for all items. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func transformFactory, IObservable forceTransform) + where TKey : notnull { - if (left == null) - { - throw new ArgumentNullException(nameof(left)); - } + return source.Transform((cur, _, _) => transformFactory(cur), forceTransform.ForForced()); + } - if (right == null) + /// + /// Projects each update item to a new form using the specified transform function. + /// + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Invoke to force a new transform for all items.# + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func transformFactory, IObservable forceTransform) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(source)); } - if (rightKeySelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - if (resultSelector == null) + if (forceTransform is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(forceTransform)); } - return left.RightJoinMany(right, rightKeySelector, (leftKey, leftValue, rightValue) => resultSelector(leftValue, rightValue)); + return source.Transform((cur, _, key) => transformFactory(cur, key), forceTransform.ForForced()); } /// - /// Groups the right data source and joins the two sources matching them using the specified key selector,, taking all right values and combining any matching left values. - /// This is the equivalent of SQL left join. + /// Projects each update item to a new form using the specified transform function. /// - /// The object type of the left datasource - /// The key type of the left datasource - /// The object type of the right datasource - /// The key type of the right datasource - /// The resulting object which - /// The left data source - /// The right data source. - /// Specify the foreign key on the right datasource - /// The result selector.used to transform the combined data into. Example (key, left, right) => new CustomObject(key, left, right) - /// - /// - public static IObservable> RightJoinMany(this IObservable> left, - [NotNull] IObservable> right, - [NotNull] Func rightKeySelector, - [NotNull] Func, IGrouping, TDestination> resultSelector) + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Invoke to force a new transform for all items.# + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func, TKey, TDestination> transformFactory, IObservable forceTransform) + where TKey : notnull { - if (left == null) - { - throw new ArgumentNullException(nameof(left)); - } - - if (right == null) + if (source is null) { - throw new ArgumentNullException(nameof(right)); + throw new ArgumentNullException(nameof(source)); } - if (rightKeySelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(rightKeySelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - if (resultSelector == null) + if (forceTransform is null) { - throw new ArgumentNullException(nameof(resultSelector)); + throw new ArgumentNullException(nameof(forceTransform)); } - return new RightJoinMany(left, right, rightKeySelector, resultSelector).Run(); + return source.Transform(transformFactory, forceTransform.ForForced()); } - #endregion - - #region Populate into an observable cache - /// - /// Populates a source into the specified cache. + /// Projects each update item to a new form using the specified transform function. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// The detination. - /// - /// - /// source + /// The transform factory. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// + /// source /// or - /// detination - /// - public static IDisposable PopulateInto(this IObservable> source, ISourceCache detination) + /// transformFactory. + public static IObservable> TransformAsync(this IObservable> source, Func> transformFactory, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (detination == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(detination)); + throw new ArgumentNullException(nameof(transformFactory)); } - return source.Subscribe(changes => detination.Edit(updater => updater.Clone(changes))); + return source.TransformAsync((current, _, _) => transformFactory(current), forceTransform); } /// - /// Populates a source into the specified cache + /// Projects each update item to a new form using the specified transform function. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// The detination. - /// + /// The transform factory. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// /// source /// or - /// detination - public static IDisposable PopulateInto(this IObservable> source, IIntermediateCache detination) + /// transformFactory. + public static IObservable> TransformAsync(this IObservable> source, Func> transformFactory, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (detination == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(detination)); + throw new ArgumentNullException(nameof(transformFactory)); } - return source.Subscribe(changes => detination.Edit(updater => updater.Clone(changes))); + return source.TransformAsync((current, _, key) => transformFactory(current, key), forceTransform); } /// - /// Populate a cache from an observable stream. + /// Projects each update item to a new form using the specified transform function. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// The observable. - /// - /// - /// source + /// The transform factory. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// + /// source /// or - /// keySelector - /// - public static IDisposable PopulateFrom(this ISourceCache source, IObservable> observable) + /// transformFactory. + public static IObservable> TransformAsync(this IObservable> source, Func, TKey, Task> transformFactory, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return observable.Subscribe(source.AddOrUpdate); + if (transformFactory is null) + { + throw new ArgumentNullException(nameof(transformFactory)); + } + + return new TransformAsync(source, transformFactory, null, forceTransform).Run(); } /// - /// Populate a cache from an observable stream. + /// Equivalent to a select many transform. To work, the key must individually identify each child. /// - /// The type of the object. - /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable with the transformed change set. /// The source. - /// The observable. - /// - /// - /// source - /// or - /// keySelector - /// - public static IDisposable PopulateFrom(this ISourceCache source, IObservable observable) + /// Will select a enumerable of values. + /// The key selector which must be unique across all. + public static IObservable> TransformMany(this IObservable> source, Func> manySelector, Func keySelector) + where TSourceKey : notnull + where TDestinationKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return observable.Subscribe(source.AddOrUpdate); + return new TransformMany(source, manySelector, keySelector).Run(); } - #endregion - - #region AsObservableCache / Connect - /// - /// Converts the source to an read only observable cache + /// Flatten the nested observable collection, and subsequently observe observable collection changes. /// - /// The type of the object. - /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable with the transformed change set. /// The source. - /// - /// source - public static IObservableCache AsObservableCache(this IObservableCache source) + /// Will select a enumerable of values. + /// The key selector which must be unique across all. + public static IObservable> TransformMany(this IObservable> source, Func> manySelector, Func keySelector) + where TSourceKey : notnull + where TDestinationKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + return new TransformMany(source, manySelector, keySelector).Run(); + } - return new AnonymousObservableCache(source); + /// + /// Flatten the nested observable collection, and subsequently observe observable collection changes. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable with the transformed change set. + /// The source. + /// Will select a enumerable of values. + /// The key selector which must be unique across all. + public static IObservable> TransformMany(this IObservable> source, Func> manySelector, Func keySelector) + where TSourceKey : notnull + where TDestinationKey : notnull + { + return new TransformMany(source, manySelector, keySelector).Run(); } /// - /// Converts the source to a readonly observable cache + /// Projects each update item to a new form using the specified transform function, + /// providing an error handling action to safely handle transform errors without killing the stream. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// if set to true all methods are synchronised. There is no need to apply locking when the consumer can be sure the the read / write operations are already synchronised - /// - /// source - public static IObservableCache AsObservableCache(this IObservable> source, bool applyLocking = true) + /// The transform factory. + /// Provides the option to safely handle errors without killing the stream. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (applyLocking) + if (transformFactory is null) { - return new AnonymousObservableCache(source); + throw new ArgumentNullException(nameof(transformFactory)); } - return new LockFreeObservableCache(source); - } - - #endregion + if (errorHandler is null) + { + throw new ArgumentNullException(nameof(errorHandler)); + } - #region Populate changetset from observables + return source.TransformSafe((current, _, _) => transformFactory(current), errorHandler, forceTransform.ForForced()); + } /// - /// Converts the observable to an observable changeset. - /// Change set observes observable change events. + /// Projects each update item to a new form using the specified transform function, + /// providing an error handling action to safely handle transform errors without killing the stream. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// The key selector. - /// Specify on a per object level the maximum time before an object expires from a cache - /// Remove the oldest items when the size has reached this limit - /// The scheduler (only used for time expiry). - /// + /// The transform factory. + /// Provides the option to safely handle errors without killing the stream. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// /// source /// or - /// keySelector - public static IObservable> ToObservableChangeSet( - this IObservable source, - Func keySelector, - Func expireAfter = null, - int limitSizeTo = -1, - IScheduler scheduler = null) + /// transformFactory. + public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(keySelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - return new ToObservableChangeSet(source, keySelector, expireAfter, limitSizeTo, scheduler).Run(); + if (errorHandler is null) + { + throw new ArgumentNullException(nameof(errorHandler)); + } + + return source.TransformSafe((current, _, key) => transformFactory(current, key), errorHandler, forceTransform); } /// - /// Converts the observable to an observable changeset. - /// Change set observes observable change events. + /// Projects each update item to a new form using the specified transform function, + /// providing an error handling action to safely handle transform errors without killing the stream. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// The key selector. - /// Specify on a per object level the maximum time before an object expires from a cache - /// Remove the oldest items when the size has reached this limit - /// The scheduler (only used for time expiry). - /// An observable changeset + /// The transform factory. + /// Provides the option to safely handle errors without killing the stream. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// /// source /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable> source, Func keySelector, - Func expireAfter = null, - int limitSizeTo = -1, - IScheduler scheduler = null) + /// transformFactory. + public static IObservable> TransformSafe(this IObservable> source, Func, TKey, TDestination> transformFactory, Action> errorHandler, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(keySelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - return new ToObservableChangeSet(source, keySelector, expireAfter, limitSizeTo, scheduler).Run(); - } + if (errorHandler is null) + { + throw new ArgumentNullException(nameof(errorHandler)); + } - #endregion + if (forceTransform is not null) + { + return new TransformWithForcedTransform(source, transformFactory, forceTransform, errorHandler).Run(); + } - #region Size / time limiters + return new Transform(source, transformFactory, errorHandler).Run(); + } /// - /// Limits the number of records in the cache to the size specified. When the size is reached - /// the oldest items are removed from the cache + /// Projects each update item to a new form using the specified transform function, + /// providing an error handling action to safely handle transform errors without killing the stream. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. + /// The type of the key. + /// The source. + /// The transform factory. + /// Provides the option to safely handle errors without killing the stream. + /// Invoke to force a new transform for all items. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable forceTransform) + where TKey : notnull + { + return source.TransformSafe((cur, _, _) => transformFactory(cur), errorHandler, forceTransform.ForForced()); + } + + /// + /// Projects each update item to a new form using the specified transform function, + /// providing an error handling action to safely handle transform errors without killing the stream. + /// + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// The size limit. - /// The scheduler. - /// - /// source - /// Size limit must be greater than zero - public static IObservable>> LimitSizeTo(this ISourceCache source, int sizeLimit, IScheduler scheduler = null) + /// The transform factory. + /// Provides the option to safely handle errors without killing the stream. + /// Invoke to force a new transform for all items.# + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, IObservable forceTransform) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (sizeLimit <= 0) + if (transformFactory is null) { - throw new ArgumentException("Size limit must be greater than zero", nameof(sizeLimit)); + throw new ArgumentNullException(nameof(transformFactory)); } - return Observable.Create>>(observer => + if (forceTransform is null) { - long orderItemWasAdded = -1; - var sizeLimiter = new SizeLimiter(sizeLimit); + throw new ArgumentNullException(nameof(forceTransform)); + } - return source.Connect() - .Finally(observer.OnCompleted) - .ObserveOn(scheduler ?? Scheduler.Default) - .Transform((t, v) => new ExpirableItem(t, v, DateTime.Now, Interlocked.Increment(ref orderItemWasAdded))) - .Select(sizeLimiter.CloneAndReturnExpiredOnly) - .Where(expired => expired.Length != 0) - .Subscribe(toRemove => - { - try - { - source.Remove(toRemove.Select(kv => kv.Key)); - observer.OnNext(toRemove); - } - catch (Exception ex) - { - observer.OnError(ex); - } - }); - }); + return source.TransformSafe((cur, _, key) => transformFactory(cur, key), errorHandler, forceTransform.ForForced()); } /// - /// Automatically removes items from the cache after the time specified by - /// the time selector elapses. + /// Projects each update item to a new form using the specified transform function, + /// providing an error handling action to safely handle transform errors without killing the stream. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. - /// The cache. - /// The time selector. Return null if the item should never be removed - /// The scheduler to perform the work on. - /// An observable of anumerable of the kev values which has been removed + /// The source. + /// The transform factory. + /// Provides the option to safely handle errors without killing the stream. + /// Invoke to force a new transform for all items.# + /// + /// A transformed update collection. + /// /// source /// or - /// timeSelector - public static IObservable>> ExpireAfter(this ISourceCache source, - Func timeSelector, IScheduler scheduler = null) + /// transformFactory. + public static IObservable> TransformSafe(this IObservable> source, Func, TKey, TDestination> transformFactory, Action> errorHandler, IObservable forceTransform) + where TKey : notnull { - return source.ExpireAfter(timeSelector, null, scheduler); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - /// - /// Automatically removes items from the cache after the time specified by - /// the time selector elapses. - /// - /// The type of the object. - /// The type of the key. - /// The cache. - /// The time selector. Return null if the item should never be removed - /// A polling interval. Since multiple timer subscriptions can be expensive, - /// it may be worth setting the interval . - /// - /// An observable of anumerable of the kev values which has been removed - /// source - /// or - /// timeSelector - public static IObservable>> ExpireAfter(this ISourceCache source, - Func timeSelector, TimeSpan? interval = null) - { - return ExpireAfter(source, timeSelector, interval, Scheduler.Default); + if (transformFactory is null) + { + throw new ArgumentNullException(nameof(transformFactory)); + } + + if (forceTransform is null) + { + throw new ArgumentNullException(nameof(forceTransform)); + } + + return source.TransformSafe(transformFactory, errorHandler, forceTransform.ForForced()); } /// - /// Automatically removes items from the cache after the time specified by - /// the time selector elapses. + /// Projects each update item to a new form using the specified transform function. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. - /// The cache. - /// The time selector. Return null if the item should never be removed - /// A polling interval. Since multiple timer subscriptions can be expensive, - /// it may be worth setting the interval. - /// - /// The scheduler. - /// An observable of anumerable of the kev values which has been removed + /// The source. + /// The transform factory. + /// The error handler. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// /// source /// or - /// timeSelector - public static IObservable>> ExpireAfter(this ISourceCache source, - Func timeSelector, TimeSpan? pollingInterval, IScheduler scheduler) + /// transformFactory. + public static IObservable> TransformSafeAsync(this IObservable> source, Func> transformFactory, Action> errorHandler, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (timeSelector == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(timeSelector)); + throw new ArgumentNullException(nameof(transformFactory)); } - return Observable.Create>>(observer => + if (errorHandler is null) { - scheduler = scheduler ?? Scheduler.Default; - return source.Connect() - .ForExpiry(timeSelector, pollingInterval, scheduler) - .Finally(observer.OnCompleted) - .Subscribe(toRemove => - { - try - { - //remove from cache and notify which items have been auto removed - var keyValuePairs = toRemove as KeyValuePair[] ?? toRemove.AsArray(); - if (keyValuePairs.Length == 0) - { - return; - } + throw new ArgumentNullException(nameof(errorHandler)); + } - source.Remove(keyValuePairs.Select(kv => kv.Key)); - observer.OnNext(keyValuePairs); - } - catch (Exception ex) - { - observer.OnError(ex); - } - }); - }); + return source.TransformSafeAsync((current, _, _) => transformFactory(current), errorHandler, forceTransform); } - #endregion - - #region Convenience update methods - /// - /// Loads the cache with the specified items in an optimised manner i.e. calculates the differences between the old and new items - /// in the list and amends only the differences + /// Projects each update item to a new form using the specified transform function. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// - /// The equality comparer used to determine whether a new item is the same as an existing cached item - /// source - public static void EditDiff([NotNull] this ISourceCache source, - [NotNull] IEnumerable alltems, - [NotNull] IEqualityComparer equalityComparer) + /// The transform factory. + /// The error handler. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> TransformSafeAsync(this IObservable> source, Func> transformFactory, Action> errorHandler, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (alltems == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(alltems)); + throw new ArgumentNullException(nameof(transformFactory)); } - if (equalityComparer == null) + if (errorHandler is null) { - throw new ArgumentNullException(nameof(equalityComparer)); + throw new ArgumentNullException(nameof(errorHandler)); } - source.EditDiff(alltems, equalityComparer.Equals); + return source.TransformSafeAsync((current, _, key) => transformFactory(current, key), errorHandler, forceTransform); } /// - /// Loads the cache with the specified items in an optimised manner i.e. calculates the differences between the old and new items - /// in the list and amends only the differences + /// Projects each update item to a new form using the specified transform function. /// - /// The type of the object. + /// The type of the destination. + /// The type of the source. /// The type of the key. /// The source. - /// - /// Expression to determine whether an item's value is equal to the old value (current, previous) => current.Version == previous.Version - /// source - public static void EditDiff([NotNull] this ISourceCache source, - [NotNull] IEnumerable alltems, - [NotNull] Func areItemsEqual) + /// The transform factory. + /// The error handler. + /// Invoke to force a new transform for items matching the selected objects. + /// + /// A transformed update collection. + /// + /// source + /// or + /// transformFactory. + public static IObservable> TransformSafeAsync(this IObservable> source, Func, TKey, Task> transformFactory, Action> errorHandler, IObservable>? forceTransform = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (alltems == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(alltems)); + throw new ArgumentNullException(nameof(transformFactory)); } - if (areItemsEqual == null) + if (errorHandler is null) { - throw new ArgumentNullException(nameof(areItemsEqual)); + throw new ArgumentNullException(nameof(errorHandler)); } - var editDiff = new EditDiff(source, areItemsEqual); - editDiff.Edit(alltems); + return new TransformAsync(source, transformFactory, errorHandler, forceTransform).Run(); } /// - /// Adds or updates the cache with the specified item. + /// Transforms the object to a fully recursive tree, create a hierarchy based on the pivot function. /// /// The type of the object. /// The type of the key. /// The source. - /// The item. - /// source - public static void AddOrUpdate(this ISourceCache source, TObject item) + /// The pivot on. + /// Observable to change the underlying predicate. + /// An observable which will emit change sets. + public static IObservable, TKey>> TransformToTree(this IObservable> source, Func pivotOn, IObservable, bool>>? predicateChanged = null) + where TKey : notnull + where TObject : class { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.AddOrUpdate(item)); + if (pivotOn is null) + { + throw new ArgumentNullException(nameof(pivotOn)); + } + + return new TreeBuilder(source, pivotOn, predicateChanged).Run(); } /// - /// Adds or updates the cache with the specified item. + /// Converts moves changes to remove + add. /// /// The type of the object. /// The type of the key. /// The source. - /// The item. - /// The equality comparer used to determine whether a new item is the same as an existing cached item - /// source - public static void AddOrUpdate(this ISourceCache source, TObject item, IEqualityComparer equalityComparer) + /// the same SortedChangeSets, except all moves are replaced with remove + add. + public static IObservable> TreatMovesAsRemoveAdd(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.AddOrUpdate(item, equalityComparer)); + IEnumerable> ReplaceMoves(IChangeSet items) + { + foreach (var change in items) + { + if (change.Reason == ChangeReason.Moved) + { + yield return new Change(ChangeReason.Remove, change.Key, change.Current, change.PreviousIndex); + + yield return new Change(ChangeReason.Add, change.Key, change.Current, change.CurrentIndex); + } + else + { + yield return change; + } + } + } + + return source.Select(changes => new SortedChangeSet(changes.SortedItems, ReplaceMoves(changes))); } /// - /// - /// Adds or updates the cache with the specified items. - /// + /// Produces a boolean observable indicating whether the latest resulting value from all of the specified observables matches + /// the equality condition. The observable is re-evaluated whenever + /// + /// i) The cache changes + /// or ii) The inner observable changes. /// /// The type of the object. /// The type of the key. + /// The type of the value. /// The source. - /// The items. - /// source - public static void AddOrUpdate(this ISourceCache source, IEnumerable items) + /// Selector which returns the target observable. + /// The equality condition. + /// An observable which boolean values indicating if true. + /// source. + public static IObservable TrueForAll(this IObservable> source, Func> observableSelector, Func equalityCondition) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - source.Edit(updater => updater.AddOrUpdate(items)); + return source.TrueFor(observableSelector, items => items.All(o => o.LatestValue.HasValue && equalityCondition(o.LatestValue.Value))); } /// - /// Removes the specified item from the cache. + /// Produces a boolean observable indicating whether the latest resulting value from all of the specified observables matches + /// the equality condition. The observable is re-evaluated whenever /// - /// If the item is not contained in the cache then the operation does nothing. + /// i) The cache changes + /// or ii) The inner observable changes. /// /// The type of the object. /// The type of the key. + /// The type of the value. /// The source. - /// The item. - /// source - public static void Remove(this ISourceCache source, TObject item) + /// Selector which returns the target observable. + /// The equality condition. + /// An observable which boolean values indicating if true. + /// source. + public static IObservable TrueForAll(this IObservable> source, Func> observableSelector, Func equalityCondition) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - source.Edit(updater => updater.Remove(item)); + return source.TrueFor(observableSelector, items => items.All(o => o.LatestValue.HasValue && equalityCondition(o.Item, o.LatestValue.Value))); } /// - /// Removes the specified key from the cache. - /// If the item is not contained in the cache then the operation does nothing. + /// Produces a boolean observable indicating whether the resulting value of whether any of the specified observables matches + /// the equality condition. The observable is re-evaluated whenever + /// i) The cache changes. + /// or ii) The inner observable changes. /// /// The type of the object. /// The type of the key. + /// The type of the value. /// The source. - /// The key. - /// source - public static void Remove(this ISourceCache source, TKey key) + /// The observable selector. + /// The equality condition. + /// An observable which boolean values indicating if true. + /// + /// source + /// or + /// observableSelector + /// or + /// equalityCondition. + /// + public static IObservable TrueForAny(this IObservable> source, Func> observableSelector, Func equalityCondition) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - source.Edit(updater => updater.Remove(key)); + return source.TrueFor(observableSelector, items => items.Any(o => o.LatestValue.HasValue && equalityCondition(o.Item, o.LatestValue.Value))); } /// - /// Removes the specified key from the cache. - /// If the item is not contained in the cache then the operation does nothing. + /// Produces a boolean observable indicating whether the resulting value of whether any of the specified observables matches + /// the equality condition. The observable is re-evaluated whenever + /// i) The cache changes. + /// or ii) The inner observable changes. /// /// The type of the object. /// The type of the key. + /// The type of the value. /// The source. - /// The key. - /// source - public static void RemoveKey(this ISourceCache source, TKey key) + /// The observable selector. + /// The equality condition. + /// An observable which boolean values indicating if true. + /// + /// source + /// or + /// observableSelector + /// or + /// equalityCondition. + /// + public static IObservable TrueForAny(this IObservable> source, Func> observableSelector, Func equalityCondition) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.RemoveKey(key)); + if (observableSelector is null) + { + throw new ArgumentNullException(nameof(observableSelector)); + } + + if (equalityCondition is null) + { + throw new ArgumentNullException(nameof(equalityCondition)); + } + + return source.TrueFor(observableSelector, items => items.Any(o => o.LatestValue.HasValue && equalityCondition(o.LatestValue.Value))); } /// - /// Removes the specified items from the cache. - /// - /// Any items not contained in the cache are ignored + /// Updates the index for an object which implements IIndexAware. /// /// The type of the object. /// The type of the key. /// The source. - /// The items. - /// source - public static void Remove(this ISourceCache source, IEnumerable items) + /// An observable which emits the sorted change set. + public static IObservable> UpdateIndex(this IObservable> source) + where TKey : notnull + where TObject : IIndexAware { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - source.Edit(updater => updater.Remove(items)); + return source.Do(changes => changes.SortedItems.Select((update, index) => new { update, index }).ForEach(u => u.update.Value.Index = u.index)); } /// - /// Removes the specified keys from the cache. - /// - /// Any keys not contained in the cache are ignored + /// Virtualises the underlying data from the specified source. /// /// The type of the object. /// The type of the key. /// The source. - /// The keys. - /// source - public static void Remove(this ISourceCache source, IEnumerable keys) + /// The virirtualising requests. + /// An observable which will emit virtual change sets. + /// source. + public static IObservable> Virtualise(this IObservable> source, IObservable virtualRequests) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.Remove(keys)); + if (virtualRequests is null) + { + throw new ArgumentNullException(nameof(virtualRequests)); + } + + return new Virtualise(source, virtualRequests).Run(); } /// - /// Removes the specified keys from the cache. - /// - /// Any keys not contained in the cache are ignored + /// Returns an observable of any updates which match the specified key, proceeded with the initial cache state. /// /// The type of the object. /// The type of the key. /// The source. - /// The keys. - /// source - public static void RemoveKeys(this ISourceCache source, IEnumerable keys) + /// The key. + /// An observable which emits the change. + public static IObservable> Watch(this IObservable> source, TKey key) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.RemoveKeys(keys)); + return source.SelectMany(updates => updates).Where(update => update.Key.Equals(key)); } /// - /// Clears all data + /// Watches updates for a single value matching the specified key. /// /// The type of the object. /// The type of the key. /// The source. - /// source - public static void Clear(this ISourceCache source) + /// The key. + /// An observable which emits the object value. + /// source. + public static IObservable WatchValue(this IObservableCache source, TKey key) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.Clear()); + return source.Watch(key).Select(u => u.Current); } /// - /// Signal observers to re-evaluate the specified item. + /// Watches updates for a single value matching the specified key. /// /// The type of the object. /// The type of the key. /// The source. - /// The item. - /// source - public static void Refresh(this ISourceCache source, TObject item) + /// The key. + /// An observable which emits the object value. + /// source. + public static IObservable WatchValue(this IObservable> source, TKey key) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.Refresh(item)); + return source.Watch(key).Select(u => u.Current); } /// - /// Signal observers to re-evaluate the specified items. + /// Watches each item in the collection and notifies when any of them has changed. /// /// The type of the object. /// The type of the key. /// The source. - /// The items. - /// source - public static void Refresh(this ISourceCache source, IEnumerable items) + /// specify properties to Monitor, or omit to monitor all property changes. + /// An observable which emits the object which has had a property changed. + public static IObservable WhenAnyPropertyChanged(this IObservable> source, params string[] propertiesToMonitor) + where TKey : notnull + where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.Refresh(items)); + return source.MergeMany(t => t.WhenAnyPropertyChanged(propertiesToMonitor)); } /// - /// Signal observers to re-evaluate the all items. + /// Watches each item in the collection and notifies when any of them has changed. /// /// The type of the object. /// The type of the key. + /// The type of the value. /// The source. - /// source - public static void Refresh(this ISourceCache source) + /// The property accessor. + /// if set to true [notify on initial value]. + /// An observable which emits a property when it has changed. + public static IObservable> WhenPropertyChanged(this IObservable> source, Expression> propertyAccessor, bool notifyOnInitialValue = true) + where TKey : notnull + where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.Refresh()); + if (propertyAccessor is null) + { + throw new ArgumentNullException(nameof(propertyAccessor)); + } + + return source.MergeMany(t => t.WhenPropertyChanged(propertyAccessor, notifyOnInitialValue)); } /// - /// Signal observers to re-evaluate the specified item. + /// Watches each item in the collection and notifies when any of them has changed. /// /// The type of the object. /// The type of the key. + /// The type of the value. /// The source. - /// The item. - /// source - [Obsolete(Constants.EvaluateIsDead)] - public static void Evaluate(this ISourceCache source, TObject item) + /// The property accessor. + /// if set to true [notify on initial value]. + /// An observable which emits a value when it has changed. + public static IObservable WhenValueChanged(this IObservable> source, Expression> propertyAccessor, bool notifyOnInitialValue = true) + where TKey : notnull + where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.Refresh(item)); + if (propertyAccessor is null) + { + throw new ArgumentNullException(nameof(propertyAccessor)); + } + + return source.MergeMany(t => t.WhenChanged(propertyAccessor, notifyOnInitialValue)); } /// - /// Signal observers to re-evaluate the specified items. + /// Includes changes for the specified reasons only. /// /// The type of the object. /// The type of the key. /// The source. - /// The items. - /// source - [Obsolete(Constants.EvaluateIsDead)] - public static void Evaluate(this ISourceCache source, IEnumerable items) + /// The reasons. + /// An observable which emits a change set with items matching the reasons. + /// reasons. + /// Must select at least on reason. + public static IObservable> WhereReasonsAre(this IObservable> source, params ChangeReason[] reasons) + where TKey : notnull { - if (source == null) + if (reasons is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(reasons)); } - source.Edit(updater => updater.Refresh(items)); + if (reasons.Length == 0) + { + throw new ArgumentException("Must select at least one reason"); + } + + var hashed = new HashSet(reasons); + + return source.Select(updates => new ChangeSet(updates.Where(u => hashed.Contains(u.Reason)))).NotEmpty(); } /// - /// Removes the specified key from the cache. - /// If the item is not contained in the cache then the operation does nothing. + /// Excludes updates for the specified reasons. /// /// The type of the object. /// The type of the key. /// The source. - /// - /// The key. - /// source - public static void AddOrUpdate(this IIntermediateCache source, TObject item, TKey key) + /// The reasons. + /// An observable which emits a change set with items not matching the reasons. + /// reasons. + /// Must select at least on reason. + public static IObservable> WhereReasonsAreNot(this IObservable> source, params ChangeReason[] reasons) + where TKey : notnull { - if (source == null) + if (reasons is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(reasons)); } - if (item == null) + if (reasons.Length == 0) { - throw new ArgumentNullException(nameof(item)); + throw new ArgumentException("Must select at least one reason"); } - source.Edit(updater => updater.AddOrUpdate(item, key)); + var hashed = new HashSet(reasons); + + return source.Select(updates => new ChangeSet(updates.Where(u => !hashed.Contains(u.Reason)))).NotEmpty(); } /// - /// Signal observers to re-evaluate the all items. + /// Apply a logical Xor operator between the collections. + /// Items which are only in one of the sources are included in the result. /// /// The type of the object. /// The type of the key. /// The source. - /// source - [Obsolete(Constants.EvaluateIsDead)] - public static void Evaluate(this ISourceCache source) + /// The others. + /// An observable which emits a change set. + /// + /// source + /// or + /// others. + /// + public static IObservable> Xor(this IObservable> source, params IObservable>[] others) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(updater => updater.Refresh()); + if (others is null || others.Length == 0) + { + throw new ArgumentNullException(nameof(others)); + } + + return source.Combine(CombineOperator.Xor, others); } /// - /// Removes the specified key from the cache. - /// If the item is not contained in the cache then the operation does nothing. + /// Apply a logical Xor operator between the collections. + /// Items which are only in one of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The source. - /// The key. - /// source - public static void Remove(this IIntermediateCache source, TKey key) + /// The source. + /// An observable which emits a change set. + /// + /// source + /// or + /// others. + /// + public static IObservable> Xor(this ICollection>> sources) + where TKey : notnull { - if (source == null) + if (sources is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(sources)); } - source.Edit(updater => updater.Remove(key)); + return sources.Combine(CombineOperator.Xor); } /// - /// Removes the specified keys from the cache. - /// - /// Any keys not contained in the cache are ignored + /// Dynamically apply a logical Xor operator between the items in the outer observable list. + /// Items which are only in one of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The source. - /// The keys. - /// source - public static void Remove(this IIntermediateCache source, IEnumerable keys) + /// The source. + /// An observable which emits a change set. + public static IObservable> Xor(this IObservableList>> sources) + where TKey : notnull { - if (source == null) + if (sources is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(sources)); } - source.Edit(updater => updater.Remove(keys)); + return sources.Combine(CombineOperator.Xor); } /// - /// Clears all items from the cache + /// Dynamically apply a logical Xor operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The source. - /// source - public static void Clear(this IIntermediateCache source) + /// The source. + /// An observable which emits a change set. + public static IObservable> Xor(this IObservableList> sources) + where TKey : notnull { - if (source == null) + if (sources is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(sources)); } - source.Edit(updater => updater.Clear()); + return sources.Combine(CombineOperator.Xor); } /// - /// Clears all data + /// Dynamically apply a logical Xor operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// /// The type of the object. /// The type of the key. - /// The source. - /// source - public static void Clear(this LockFreeObservableCache source) + /// The source. + /// An observable which emits a change set. + public static IObservable> Xor(this IObservableList> sources) + where TKey : notnull { - if (source == null) + if (sources is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(sources)); } - source.Edit(updater => updater.Clear()); + return sources.Combine(CombineOperator.Xor); } /// - /// Populates a source into the specified cache. + /// Automatically removes items from the cache after the time specified by + /// the time selector elapses. /// /// The type of the object. /// The type of the key. - /// The source. - /// The detination. - /// - /// - /// source + /// The cache. + /// The time selector. Return null if the item should never be removed. + /// A polling interval. Since multiple timer subscriptions can be expensive, + /// it may be worth setting the interval. + /// + /// The scheduler. + /// An observable of enumerable of the key values which has been removed. + /// source /// or - /// detination - /// - public static IDisposable PopulateInto(this IObservable> source, LockFreeObservableCache detination) + /// timeSelector. + internal static IObservable>> ForExpiry(this IObservable> source, Func timeSelector, TimeSpan? interval, IScheduler scheduler) + where TKey : notnull + { + return new TimeExpirer(source, timeSelector, interval, scheduler).ForExpiry(); + } + + private static IObservable> Combine(this IObservableList> source, CombineOperator type) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (detination == null) + return Observable.Create>( + observer => + { + var connections = source.Connect().Transform(x => x.Connect()).AsObservableList(); + var subscriber = connections.Combine(type).SubscribeSafe(observer); + return new CompositeDisposable(connections, subscriber); + }); + } + + private static IObservable> Combine(this IObservableList> source, CombineOperator type) + where TKey : notnull + { + if (source is null) { - throw new ArgumentNullException(nameof(detination)); + throw new ArgumentNullException(nameof(source)); } - return source.Subscribe(changes => detination.Edit(updater => updater.Clone(changes))); + return Observable.Create>( + observer => + { + var connections = source.Connect().Transform(x => x.Connect()).AsObservableList(); + var subscriber = connections.Combine(type).SubscribeSafe(observer); + return new CompositeDisposable(connections, subscriber); + }); } - #endregion + private static IObservable> Combine(this IObservableList>> source, CombineOperator type) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - #region Switch + return new DynamicCombiner(source, type).Run(); + } - /// - /// Transforms an observable sequence of observable caches into a single sequence - /// producing values only from the most recent observable sequence. - /// Each time a new inner observable sequence is received, unsubscribe from the - /// previous inner observable sequence and clear the existing result set - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - /// The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received. - /// - /// - /// is null. - public static IObservable> Switch(this IObservable> sources) + private static IObservable> Combine(this ICollection>> sources, CombineOperator type) + where TKey : notnull { - if (sources == null) + if (sources is null) { throw new ArgumentNullException(nameof(sources)); } - return sources.Select(cache => cache.Connect()).Switch(); - } + return Observable.Create>( + observer => + { + void UpdateAction(IChangeSet updates) + { + try + { + observer.OnNext(updates); + } + catch (Exception ex) + { + observer.OnError(ex); + } + } - /// - /// Transforms an observable sequence of observable changes sets into an observable sequence - /// producing values only from the most recent observable sequence. - /// Each time a new inner observable sequence is received, unsubscribe from the - /// previous inner observable sequence and clear the existing resukt set - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// - /// The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received. - /// - /// - /// is null. + IDisposable subscriber = Disposable.Empty; + try + { + var combiner = new Combiner(type, UpdateAction); + subscriber = combiner.Subscribe(sources.ToArray()); + } + catch (Exception ex) + { + observer.OnError(ex); + observer.OnCompleted(); + } - public static IObservable> Switch(this IObservable>> sources) + return subscriber; + }); + } + + private static IObservable> Combine(this IObservable> source, CombineOperator type, params IObservable>[] combineTarget) + where TKey : notnull { - if (sources == null) + if (combineTarget is null) { - throw new ArgumentNullException(nameof(sources)); + throw new ArgumentNullException(nameof(combineTarget)); } - return new Switch(sources).Run(); + return Observable.Create>( + observer => + { + void UpdateAction(IChangeSet updates) + { + try + { + observer.OnNext(updates); + } + catch (Exception ex) + { + observer.OnError(ex); + observer.OnCompleted(); + } + } + + IDisposable subscriber = Disposable.Empty; + try + { + var list = combineTarget.ToList(); + list.Insert(0, source); + + var combiner = new Combiner(type, UpdateAction); + subscriber = combiner.Subscribe(list.ToArray()); + } + catch (Exception ex) + { + observer.OnError(ex); + observer.OnCompleted(); + } + + return subscriber; + }); + } + + private static IObservable>? ForForced(this IObservable? source) + where TKey : notnull + { + return source?.Select( + _ => + { + bool Transformer(TSource item, TKey key) => true; + return (Func)Transformer; + }); + } + private static IObservable>? ForForced(this IObservable>? source) + where TKey : notnull + { + return source?.Select( + condition => + { + bool Transformer(TSource item, TKey key) => condition(item); + return (Func)Transformer; + }); } - #endregion + private static IObservable TrueFor(this IObservable> source, Func> observableSelector, Func>, bool> collectionMatcher) + where TKey : notnull + { + return new TrueFor(source, observableSelector, collectionMatcher).Run(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/PageRequest.cs b/src/DynamicData/Cache/PageRequest.cs index 68d469094..2c54e9210 100644 --- a/src/DynamicData/Cache/PageRequest.cs +++ b/src/DynamicData/Cache/PageRequest.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,20 +10,22 @@ namespace DynamicData { /// - /// Represents a new page request + /// Represents a new page request. /// public sealed class PageRequest : IPageRequest, IEquatable { /// - /// The default page request + /// The default page request. /// public static readonly IPageRequest Default = new PageRequest(); /// - /// Represents an empty page + /// Represents an empty page. /// public static readonly IPageRequest Empty = new PageRequest(0, 0); + private static readonly IEqualityComparer _pageSizeComparerInstance = new PageSizeEqualityComparer(); + /// /// Initializes a new instance of the class. /// @@ -46,39 +48,41 @@ public PageRequest(int page, int size) } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public PageRequest() { } /// - /// The page to move to + /// Gets the default comparer. + /// + /// + /// The default comparer. + /// + [SuppressMessage("Design", "CA1822: Member can be static", Justification = "Backwards compatibilty")] + public IEqualityComparer DefaultComparer => _pageSizeComparerInstance; + + /// + /// Gets the page to move to. /// public int Page { get; } = 1; /// - /// The page size + /// Gets the page size. /// public int Size { get; } = 25; - #region Equality members - /// - public bool Equals(IPageRequest other) + public bool Equals(IPageRequest? other) { return DefaultComparer.Equals(this, other); } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (!(obj is IPageRequest)) - { - return false; - } - - return Equals((IPageRequest)obj); + return obj is IPageRequest value && Equals(value); } /// @@ -90,13 +94,12 @@ public override int GetHashCode() } } - #endregion - - #region PageSizeEqualityComparer + /// + public override string ToString() => $"Page: {Page}, Size: {Size}"; - private sealed class PageSizeEqualityComparer : IEqualityComparer + private sealed class PageSizeEqualityComparer : IEqualityComparer { - public bool Equals(IPageRequest x, IPageRequest y) + public bool Equals(IPageRequest? x, IPageRequest? y) { if (ReferenceEquals(x, y)) { @@ -121,29 +124,18 @@ public bool Equals(IPageRequest x, IPageRequest y) return x.Page == y.Page && x.Size == y.Size; } - public int GetHashCode(IPageRequest obj) + public int GetHashCode(IPageRequest? obj) { + if (obj is null) + { + return 0; + } + unchecked { return (obj.Page * 397) ^ obj.Size; } } } - - private static readonly IEqualityComparer _pageSizeComparerInstance = new PageSizeEqualityComparer(); - - /// - /// Gets the default comparer. - /// - /// - /// The default comparer. - /// - [SuppressMessage("Design", "CA1822: Member can be static", Justification = "Backwards compatibilty")] - public IEqualityComparer DefaultComparer => _pageSizeComparerInstance; - - #endregion - - /// - public override string ToString() => $"Page: {Page}, Size: {Size}"; } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/PageResponse.cs b/src/DynamicData/Cache/PageResponse.cs index 77fe5ba9d..3f8a72595 100644 --- a/src/DynamicData/Cache/PageResponse.cs +++ b/src/DynamicData/Cache/PageResponse.cs @@ -1,10 +1,12 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Operators; + // ReSharper disable once CheckNamespace namespace DynamicData { @@ -18,15 +20,15 @@ public PageResponse(int pageSize, int totalSize, int page, int pages) Pages = pages; } - public int PageSize { get; } + public static IEqualityComparer DefaultComparer { get; } = new PageResponseEqualityComparer(); public int Page { get; } public int Pages { get; } - public int TotalSize { get; } + public int PageSize { get; } - #region Equality members + public int TotalSize { get; } /// /// Indicates whether the current object is equal to another object of the same type. @@ -35,33 +37,38 @@ public PageResponse(int pageSize, int totalSize, int page, int pages) /// true if the current object is equal to the parameter; otherwise, false. /// /// An object to compare with this object. - public bool Equals(IPageResponse other) + public bool Equals(IPageResponse? other) { return DefaultComparer.Equals(this, other); } /// - /// Determines whether the specified is equal to the current . + /// Determines whether the specified is equal to the current . /// /// - /// true if the specified is equal to the current ; otherwise, false. + /// true if the specified is equal to the current ; otherwise, false. /// - /// The to compare with the current . - public override bool Equals(object obj) + /// The to compare with the current . + public override bool Equals(object? obj) { - if (!(obj is IPageResponse)) + if (obj is null) { return false; } - return Equals(obj as IPageResponse); + if (!(obj is IPageResponse pageResponse)) + { + return false; + } + + return Equals(pageResponse); } /// /// Serves as a hash function for a particular type. /// /// - /// A hash code for the current . + /// A hash code for the current . /// public override int GetHashCode() { @@ -75,13 +82,20 @@ public override int GetHashCode() } } - #endregion - - #region PageResponseEqualityComparer + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + return $"Page: {Page}, PageSize: {PageSize}, Pages: {Pages}, TotalSize: {TotalSize}"; + } - private sealed class PageResponseEqualityComparer : IEqualityComparer + private sealed class PageResponseEqualityComparer : IEqualityComparer { - public bool Equals(IPageResponse x, IPageResponse y) + public bool Equals(IPageResponse? x, IPageResponse? y) { if (ReferenceEquals(x, y)) { @@ -106,8 +120,13 @@ public bool Equals(IPageResponse x, IPageResponse y) return x.PageSize == y.PageSize && x.TotalSize == y.TotalSize && x.Page == y.Page && x.Pages == y.Pages; } - public int GetHashCode(IPageResponse obj) + public int GetHashCode(IPageResponse? obj) { + if (obj is null) + { + return 0; + } + unchecked { int hashCode = obj.PageSize; @@ -118,20 +137,5 @@ public int GetHashCode(IPageResponse obj) } } } - - public static IEqualityComparer DefaultComparer { get; } = new PageResponseEqualityComparer(); - - #endregion - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString() - { - return $"Page: {Page}, PageSize: {PageSize}, Pages: {Pages}, TotalSize: {TotalSize}"; - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/PagedChangeSet.cs b/src/DynamicData/Cache/PagedChangeSet.cs index 17a21cc7b..94083e921 100644 --- a/src/DynamicData/Cache/PagedChangeSet.cs +++ b/src/DynamicData/Cache/PagedChangeSet.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; using System.Linq; + using DynamicData.Cache.Internal; using DynamicData.Operators; + // ReSharper disable once CheckNamespace namespace DynamicData { internal sealed class PagedChangeSet : ChangeSet, IPagedChangeSet + where TKey : notnull { - public new static readonly IPagedChangeSet Empty = new PagedChangeSet(); - - public IKeyValueCollection SortedItems { get; } - public IPageResponse Response { get; } + public static readonly new IPagedChangeSet Empty = new PagedChangeSet(); public PagedChangeSet(IKeyValueCollection sortedItems, IEnumerable> updates, IPageResponse response) : base(updates) @@ -29,54 +29,39 @@ private PagedChangeSet() Response = new PageResponse(0, 0, 0, 0); } - #region Equality Members + public IPageResponse Response { get; } + + public IKeyValueCollection SortedItems { get; } /// - /// Equalses the specified other. + /// Determines if the two values equal each other. /// /// The other. - /// + /// If the page change set equals the other. public bool Equals(PagedChangeSet other) { return SortedItems.SequenceEqual(other.SortedItems); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((PagedChangeSet)obj); + return obj is PagedChangeSet value && Equals(value); } public override int GetHashCode() { - return SortedItems?.GetHashCode() ?? 0; + return SortedItems.GetHashCode(); } - #endregion - /// - /// Returns a that represents the SortedItems . + /// Returns a that represents the SortedItems . /// /// - /// A that represents the SortedItems . + /// A that represents the SortedItems . /// public override string ToString() { return $"{base.ToString()}, Response: {Response}, SortedItems: {SortedItems}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/SortOptimisations.cs b/src/DynamicData/Cache/SortOptimisations.cs index 0b44d043e..ed8a6143f 100644 --- a/src/DynamicData/Cache/SortOptimisations.cs +++ b/src/DynamicData/Cache/SortOptimisations.cs @@ -1,38 +1,39 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; + // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Flags used to specify one or more sort optimisations + /// Flags used to specify one or more sort optimisations. /// [Flags] public enum SortOptimisations { /// - /// No sorting optimisation are applied + /// No sorting optimisation are applied. /// None = 0, /// /// Specify this option if the comparer used for sorting compares immutable fields only. - /// In which case index changes can be calculated using BinarySearch rather than the expensive IndexOf + /// In which case index changes can be calculated using BinarySearch rather than the expensive IndexOf. /// ComparesImmutableValuesOnly = 1, /// /// Ignores moves because of evaluates. - /// Use for virtualisatiom or pagination + /// Use for virtualisatiom or pagination. /// IgnoreEvaluates = 2, /// - /// The insert at end then sort entire set. This can be the best algorithm for large datasets with many changes + /// The insert at end then sort entire set. This can be the best algorithm for large data sets with many changes. /// [Obsolete("This is no longer being used. Use one of the other options instead.")] InsertAtEndThenSort = 3 } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/SortReason.cs b/src/DynamicData/Cache/SortReason.cs index 7d9335c79..eeca69577 100644 --- a/src/DynamicData/Cache/SortReason.cs +++ b/src/DynamicData/Cache/SortReason.cs @@ -1,8 +1,11 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// The reason why the sorted collection has changed + /// The reason why the sorted collection has changed. /// public enum SortReason { @@ -32,4 +35,4 @@ public enum SortReason /// Reset } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/SortedChangeSet.cs b/src/DynamicData/Cache/SortedChangeSet.cs index 06c531fb8..c203ef86e 100644 --- a/src/DynamicData/Cache/SortedChangeSet.cs +++ b/src/DynamicData/Cache/SortedChangeSet.cs @@ -1,19 +1,19 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; using System.Linq; + using DynamicData.Cache.Internal; // ReSharper disable once CheckNamespace namespace DynamicData { internal class SortedChangeSet : ChangeSet, ISortedChangeSet + where TKey : notnull { - public new static readonly ISortedChangeSet Empty = new SortedChangeSet(); - - public IKeyValueCollection SortedItems { get; } + public static readonly new ISortedChangeSet Empty = new SortedChangeSet(); public SortedChangeSet(IKeyValueCollection sortedItems, IEnumerable> updates) : base(updates) @@ -26,43 +26,26 @@ private SortedChangeSet() SortedItems = new KeyValueCollection(); } - #region Equality Members + public IKeyValueCollection SortedItems { get; } public bool Equals(SortedChangeSet other) { return SortedItems.SequenceEqual(other.SortedItems); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((SortedChangeSet)obj); + return obj is SortedChangeSet value && Equals(value); } public override int GetHashCode() { - return SortedItems?.GetHashCode() ?? 0; + return SortedItems.GetHashCode(); } - #endregion - public override string ToString() { return $"SortedChangeSet. Count= {SortedItems.Count}. Updates = {Count}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/SourceCache.cs b/src/DynamicData/Cache/SourceCache.cs index ac1d5d58f..39471e678 100644 --- a/src/DynamicData/Cache/SourceCache.cs +++ b/src/DynamicData/Cache/SourceCache.cs @@ -1,67 +1,59 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; + // ReSharper disable once CheckNamespace namespace DynamicData { /// /// An observable cache which exposes an update API. Used at the root - /// of all observable chains + /// of all observable chains. /// /// The type of the object. /// The type of the key. public class SourceCache : ISourceCache + where TKey : notnull { private readonly ObservableCache _innerCache; + private bool _isDisposed; /// /// Initializes a new instance of the class. /// /// The key selector. - /// keySelector + /// keySelector. public SourceCache(Func keySelector) { KeySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector)); _innerCache = new ObservableCache(keySelector); } - #region Delegated Members - /// - public void Edit(Action> updateAction) => _innerCache.UpdateFromSource(updateAction); + public int Count => _innerCache.Count; /// public IObservable CountChanged => _innerCache.CountChanged; - /// - public IObservable> Connect(Func predicate = null) => _innerCache.Connect(predicate); - /// - public IObservable> Preview(Func predicate = null) => _innerCache.Preview(predicate); - - /// - public IObservable> Watch(TKey key) => _innerCache.Watch(key); - - /// - public int Count => _innerCache.Count; - /// public IEnumerable Items => _innerCache.Items; - /// - public IEnumerable> KeyValues => _innerCache.KeyValues; - /// public IEnumerable Keys => _innerCache.Keys; + /// public Func KeySelector { get; } /// - public Optional Lookup(TKey key) => _innerCache.Lookup(key); + public IEnumerable> KeyValues => _innerCache.KeyValues; + + /// + public IObservable> Connect(Func? predicate = null) => _innerCache.Connect(predicate); /// public void Dispose() @@ -70,6 +62,22 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + public void Edit(Action> updateAction) => _innerCache.UpdateFromSource(updateAction); + + /// + public Optional Lookup(TKey key) => _innerCache.Lookup(key); + + /// + public IObservable> Preview(Func? predicate = null) => _innerCache.Preview(predicate); + + /// + public IObservable> Watch(TKey key) => _innerCache.Watch(key); + + /// + /// Disposes of managed and unmanaged responses. + /// + /// If being called by the Dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -84,7 +92,5 @@ protected virtual void Dispose(bool isDisposing) _innerCache.Dispose(); } } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/SourceCacheEx.cs b/src/DynamicData/Cache/SourceCacheEx.cs index 842d07fbe..493e23c56 100644 --- a/src/DynamicData/Cache/SourceCacheEx.cs +++ b/src/DynamicData/Cache/SourceCacheEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,7 +8,7 @@ namespace DynamicData { /// - /// Source cache convenience extensions + /// Source cache convenience extensions. /// public static class SourceCacheEx { @@ -21,10 +21,11 @@ public static class SourceCacheEx /// The type of the destination. /// The source. /// The conversion factory. - /// + /// An observable which emits the change set. public static IObservable> Cast(this IObservableCache source, Func converter) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -32,4 +33,4 @@ public static IObservable> Cast - /// Aggregates all events and statistics for a changeset to help assertions when testing + /// Aggregates all events and statistics for a change set to help assertions when testing. /// /// The type of the object. /// The type of the key. public class ChangeSetAggregator : IDisposable + where TKey : notnull { private readonly IDisposable _disposer; + private bool _isDisposed; /// @@ -32,16 +35,17 @@ public ChangeSetAggregator(IObservable> source) Data = published.AsObservableCache(); var results = published.Subscribe(updates => Messages.Add(updates), ex => Error = ex); - var summariser = published.CollectUpdateStats().Subscribe(summary => Summary = summary, ex=>{}); + var summariser = published.CollectUpdateStats().Subscribe(summary => Summary = summary, _ => { }); var connected = published.Connect(); - _disposer = Disposable.Create(() => - { - Data.Dispose(); - connected.Dispose(); - summariser.Dispose(); - results.Dispose(); - }); + _disposer = Disposable.Create( + () => + { + Data.Dispose(); + connected.Dispose(); + summariser.Dispose(); + results.Dispose(); + }); } /// @@ -52,6 +56,14 @@ public ChangeSetAggregator(IObservable> source) /// public IObservableCache Data { get; } + /// + /// Gets the error. + /// + /// + /// The error. + /// + public Exception? Error { get; private set; } + /// /// Gets the messages. /// @@ -68,14 +80,6 @@ public ChangeSetAggregator(IObservable> source) /// public ChangeSummary Summary { get; private set; } = ChangeSummary.Empty; - /// - /// Gets the error. - /// - /// - /// The error. - /// - public Exception Error { get; private set; } - /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -85,6 +89,10 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Disposes of managed and unmanaged responses. + /// + /// If being called by the Dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -96,8 +104,8 @@ protected virtual void Dispose(bool isDisposing) if (isDisposing) { - _disposer?.Dispose(); + _disposer.Dispose(); } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Tests/DistinctChangeSetAggregator.cs b/src/DynamicData/Cache/Tests/DistinctChangeSetAggregator.cs index d8635864f..5f075cff3 100644 --- a/src/DynamicData/Cache/Tests/DistinctChangeSetAggregator.cs +++ b/src/DynamicData/Cache/Tests/DistinctChangeSetAggregator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,20 +6,21 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Diagnostics; // ReSharper disable once CheckNamespace namespace DynamicData.Tests { /// - /// Aggregates all events and statistics for a distinct changeset to help assertions when testing + /// Aggregates all events and statistics for a distinct change set to help assertions when testing. /// /// The type of the value. public class DistinctChangeSetAggregator : IDisposable + where TValue : notnull { private readonly IDisposable _disposer; - private ChangeSummary _summary = ChangeSummary.Empty; - private Exception _error; + private bool _isDisposed; /// @@ -30,19 +31,20 @@ public DistinctChangeSetAggregator(IObservable> sourc { var published = source.Publish(); - var error = published.Subscribe(updates => { }, ex => _error = ex); + var error = published.Subscribe(_ => { }, ex => Error = ex); var results = published.Subscribe(updates => Messages.Add(updates)); Data = published.AsObservableCache(); - var summariser = published.CollectUpdateStats().Subscribe(summary => _summary = summary); + var summariser = published.CollectUpdateStats().Subscribe(summary => Summary = summary); var connected = published.Connect(); - _disposer = Disposable.Create(() => - { - connected.Dispose(); - summariser.Dispose(); - results.Dispose(); - error.Dispose(); - }); + _disposer = Disposable.Create( + () => + { + connected.Dispose(); + summariser.Dispose(); + results.Dispose(); + error.Dispose(); + }); } /// @@ -51,22 +53,22 @@ public DistinctChangeSetAggregator(IObservable> sourc public IObservableCache Data { get; } /// - /// Gets the messages. + /// Gets the error. /// - public IList> Messages { get; } = new List>(); + /// + /// The error. + /// + public Exception? Error { get; private set; } /// - /// Gets the summary. + /// Gets the messages. /// - public ChangeSummary Summary => _summary; + public IList> Messages { get; } = new List>(); /// - /// Gets the error. + /// Gets the summary. /// - /// - /// The error. - /// - public Exception Error => _error; + public ChangeSummary Summary { get; private set; } = ChangeSummary.Empty; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -77,6 +79,10 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Disposes of managed and unmanaged responses. + /// + /// If being called by the Dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -88,9 +94,8 @@ protected virtual void Dispose(bool isDisposing) if (isDisposing) { - _disposer?.Dispose(); + _disposer.Dispose(); } } - } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Tests/PagedChangeSetAggregator.cs b/src/DynamicData/Cache/Tests/PagedChangeSetAggregator.cs index 65cd4b527..c25777329 100644 --- a/src/DynamicData/Cache/Tests/PagedChangeSetAggregator.cs +++ b/src/DynamicData/Cache/Tests/PagedChangeSetAggregator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,21 +6,22 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Diagnostics; // ReSharper disable once CheckNamespace namespace DynamicData.Tests { /// - /// Aggregates all events and statistics for a paged changeset to help assertions when testing + /// Aggregates all events and statistics for a paged change set to help assertions when testing. /// /// The type of the object. /// The type of the key. public class PagedChangeSetAggregator : IDisposable + where TKey : notnull { private readonly IDisposable _disposer; - private Exception _error; - private ChangeSummary _summary = ChangeSummary.Empty; + private bool _isDisposed; /// @@ -31,50 +32,51 @@ public PagedChangeSetAggregator(IObservable> sour { var published = source.Publish(); - var error = published.Subscribe(updates => { }, ex => _error = ex); + var error = published.Subscribe(_ => { }, ex => Error = ex); var results = published.Subscribe(updates => Messages.Add(updates)); Data = published.AsObservableCache(); - var summariser = published.CollectUpdateStats().Subscribe(summary => _summary = summary); + var summariser = published.CollectUpdateStats().Subscribe(summary => Summary = summary); var connected = published.Connect(); - _disposer = Disposable.Create(() => - { - connected.Dispose(); - summariser.Dispose(); - results.Dispose(); - error.Dispose(); - }); + _disposer = Disposable.Create( + () => + { + connected.Dispose(); + summariser.Dispose(); + results.Dispose(); + error.Dispose(); + }); } /// - /// The data of the steam cached inorder to apply assertions + /// Gets the data of the steam cached in-order to apply assertions. /// public IObservableCache Data { get; } /// - /// Record of all received messages. + /// Gets and error. /// /// - /// The messages. + /// The error. /// - public IList> Messages { get; } = new List>(); + public Exception? Error { get; private set; } /// - /// The aggregated change summary. + /// Gets record of all received messages. /// /// - /// The summary. + /// The messages. /// - public ChangeSummary Summary => _summary; + public IList> Messages { get; } = new List>(); /// - /// Gets and error. + /// Gets the aggregated change summary. /// /// - /// The error. + /// The summary. /// - public Exception Error => _error; + public ChangeSummary Summary { get; private set; } = ChangeSummary.Empty; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -85,6 +87,10 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Disposes of managed and unmanaged resources. + /// + /// If the method is being called by the Dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -96,8 +102,8 @@ protected virtual void Dispose(bool isDisposing) if (isDisposing) { - _disposer?.Dispose(); + _disposer.Dispose(); } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Tests/SortedChangeSetAggregator.cs b/src/DynamicData/Cache/Tests/SortedChangeSetAggregator.cs index a796d82b5..bff7d2a51 100644 --- a/src/DynamicData/Cache/Tests/SortedChangeSetAggregator.cs +++ b/src/DynamicData/Cache/Tests/SortedChangeSetAggregator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,19 +6,22 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Diagnostics; // ReSharper disable once CheckNamespace namespace DynamicData.Tests { /// - /// Aggregates all events and statistics for a sorted changeset to help assertions when testing + /// Aggregates all events and statistics for a sorted change set to help assertions when testing. /// /// The type of the object. /// The type of the key. public class SortedChangeSetAggregator : IDisposable + where TKey : notnull { private readonly IDisposable _disposer; + private bool _isDisposed; /// @@ -29,28 +32,34 @@ public SortedChangeSetAggregator(IObservable> so { var published = source.Publish(); - var error = published.Subscribe(updates => { }, ex => Error = ex); + var error = published.Subscribe(_ => { }, ex => Error = ex); var results = published.Subscribe(updates => Messages.Add(updates)); Data = published.AsObservableCache(); var summariser = published.CollectUpdateStats().Subscribe(summary => Summary = summary); var connected = published.Connect(); - _disposer = Disposable.Create(() => - { - connected.Dispose(); - summariser.Dispose(); - results.Dispose(); - error.Dispose(); - }); + _disposer = Disposable.Create( + () => + { + connected.Dispose(); + summariser.Dispose(); + results.Dispose(); + error.Dispose(); + }); } /// - /// The data of the steam cached inorder to apply assertions + /// Gets the data of the steam cached in-order to apply assertions. /// public IObservableCache Data { get; } /// - /// Record of all received messages. + /// Gets and error. + /// + public Exception? Error { get; private set; } + + /// + /// Gets record of all received messages. /// /// /// The messages. @@ -58,18 +67,13 @@ public SortedChangeSetAggregator(IObservable> so public IList> Messages { get; } = new List>(); /// - /// The aggregated change summary. + /// Gets the aggregated change summary. /// /// /// The summary. /// public ChangeSummary Summary { get; private set; } = ChangeSummary.Empty; - /// - /// Gets and error. - /// - public Exception Error { get; private set; } - /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -79,6 +83,10 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Disposes of managed and unmanaged responses. + /// + /// If being called by the Dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -90,8 +98,8 @@ protected virtual void Dispose(bool isDisposing) if (isDisposing) { - _disposer?.Dispose(); + _disposer.Dispose(); } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Tests/TestEx.cs b/src/DynamicData/Cache/Tests/TestEx.cs index 5441bb611..f4a846844 100644 --- a/src/DynamicData/Cache/Tests/TestEx.cs +++ b/src/DynamicData/Cache/Tests/TestEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,32 +8,34 @@ namespace DynamicData.Tests { /// - /// Test extensions + /// Test extensions. /// public static class TestEx { /// - /// Aggregates all events and statistics for a paged changeset to help assertions when testing + /// Aggregates all events and statistics for a paged change set to help assertions when testing. /// /// The type of the object. /// The type of the key. /// The source. - /// + /// The change set aggregator. public static ChangeSetAggregator AsAggregator(this IObservable> source) + where TKey : notnull { - return new ChangeSetAggregator(source); + return new(source); } /// - /// Aggregates all events and statistics for a distinct changeset to help assertions when testing + /// Aggregates all events and statistics for a distinct change set to help assertions when testing. /// /// The type of the value. /// The source. - /// - /// source + /// The distinct change set aggregator. + /// source. public static DistinctChangeSetAggregator AsAggregator(this IObservable> source) + where TValue : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -42,16 +44,17 @@ public static DistinctChangeSetAggregator AsAggregator(this IObs } /// - /// Aggregates all events and statistics for a sorted changeset to help assertions when testing + /// Aggregates all events and statistics for a sorted change set to help assertions when testing. /// /// The type of the object. /// The type of the key. /// The source. - /// - /// source + /// The sorted change set aggregator. + /// source. public static SortedChangeSetAggregator AsAggregator(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -60,16 +63,17 @@ public static SortedChangeSetAggregator AsAggregator - ///Aggregates all events and statistics for a virtual changeset to help assertions when testing + /// Aggregates all events and statistics for a virtual change set to help assertions when testing. /// /// The type of the object. /// The type of the key. /// The source. - /// - /// source + /// The virtual change set aggregator. + /// source. public static VirtualChangeSetAggregator AsAggregator(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -78,15 +82,16 @@ public static VirtualChangeSetAggregator AsAggregator - /// Aggregates all events and statistics for a paged changeset to help assertions when testing + /// Aggregates all events and statistics for a paged change set to help assertions when testing. /// /// The type of the object. /// The type of the key. /// The source. - /// + /// The paged change set aggregator. public static PagedChangeSetAggregator AsAggregator(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -94,4 +99,4 @@ public static PagedChangeSetAggregator AsAggregator(source); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/Tests/VirtualChangeSetAggregator.cs b/src/DynamicData/Cache/Tests/VirtualChangeSetAggregator.cs index 73d86b0f1..c95a940e9 100644 --- a/src/DynamicData/Cache/Tests/VirtualChangeSetAggregator.cs +++ b/src/DynamicData/Cache/Tests/VirtualChangeSetAggregator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,19 +6,22 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Diagnostics; // ReSharper disable once CheckNamespace namespace DynamicData.Tests { /// - /// Aggregates all events and statistics for a virtual changeset to help assertions when testing + /// Aggregates all events and statistics for a virtual change set to help assertions when testing. /// /// The type of the object. /// The type of the key. public class VirtualChangeSetAggregator : IDisposable + where TKey : notnull { private readonly IDisposable _disposer; + private bool _isDisposed; /// @@ -29,19 +32,20 @@ public VirtualChangeSetAggregator(IObservable> { var published = source.Publish(); - var error = published.Subscribe(updates => { }, ex => Error = ex); + var error = published.Subscribe(_ => { }, ex => Error = ex); var results = published.Subscribe(updates => Messages.Add(updates)); Data = published.AsObservableCache(); var summariser = published.CollectUpdateStats().Subscribe(summary => Summary = summary); var connected = published.Connect(); - _disposer = Disposable.Create(() => - { - connected.Dispose(); - summariser.Dispose(); - results.Dispose(); - error.Dispose(); - }); + _disposer = Disposable.Create( + () => + { + connected.Dispose(); + summariser.Dispose(); + results.Dispose(); + error.Dispose(); + }); } /// @@ -52,6 +56,14 @@ public VirtualChangeSetAggregator(IObservable> /// public IObservableCache Data { get; } + /// + /// Gets the error. + /// + /// + /// The error. + /// + public Exception? Error { get; private set; } + /// /// Gets the messages. /// @@ -68,14 +80,6 @@ public VirtualChangeSetAggregator(IObservable> /// public ChangeSummary Summary { get; private set; } = ChangeSummary.Empty; - /// - /// Gets the error. - /// - /// - /// The error. - /// - public Exception Error { get; private set; } - /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -85,6 +89,10 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Disposes of managed and unmanaged responses. + /// + /// If being called by the Dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -96,8 +104,8 @@ protected virtual void Dispose(bool isDisposing) if (isDisposing) { - _disposer?.Dispose(); + _disposer.Dispose(); } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/VirtualChangeSet.cs b/src/DynamicData/Cache/VirtualChangeSet.cs index b44465990..a4c17f90c 100644 --- a/src/DynamicData/Cache/VirtualChangeSet.cs +++ b/src/DynamicData/Cache/VirtualChangeSet.cs @@ -1,20 +1,19 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Cache.Internal; // ReSharper disable once CheckNamespace namespace DynamicData { internal sealed class VirtualChangeSet : ChangeSet, IVirtualChangeSet, IEquatable> + where TKey : notnull { - public new static readonly IVirtualChangeSet Empty = new VirtualChangeSet(); - - public IKeyValueCollection SortedItems { get; } - public IVirtualResponse Response { get; } + public static readonly new IVirtualChangeSet Empty = new VirtualChangeSet(); public VirtualChangeSet(IEnumerable> items, IKeyValueCollection sortedItems, IVirtualResponse response) : base(items) @@ -26,45 +25,41 @@ public VirtualChangeSet(IEnumerable> items, IKeyValueColle private VirtualChangeSet() { SortedItems = new KeyValueCollection(); - Response = new VirtualResponse(0,0,0); + Response = new VirtualResponse(0, 0, 0); } - #region Equality + public IVirtualResponse Response { get; } - public bool Equals(VirtualChangeSet other) - { - if (ReferenceEquals(null, other)) - { - return false; - } + public IKeyValueCollection SortedItems { get; } - if (ReferenceEquals(this, other)) - { - return true; - } + public static bool operator ==(VirtualChangeSet left, VirtualChangeSet right) + { + return Equals(left, right); + } - return Response.Equals(other.Response) - && Equals(SortedItems, other.SortedItems); + public static bool operator !=(VirtualChangeSet left, VirtualChangeSet right) + { + return !Equals(left, right); } - public override bool Equals(object obj) + public bool Equals(VirtualChangeSet? other) { - if (ReferenceEquals(null, obj)) + if (ReferenceEquals(null, other)) { return false; } - if (ReferenceEquals(this, obj)) + if (ReferenceEquals(this, other)) { return true; } - if (obj.GetType() != GetType()) - { - return false; - } + return Response.Equals(other.Response) && Equals(SortedItems, other.SortedItems); + } - return Equals((VirtualChangeSet)obj); + public override bool Equals(object? obj) + { + return obj is VirtualChangeSet item && Equals(item); } public override int GetHashCode() @@ -72,21 +67,9 @@ public override int GetHashCode() unchecked { int hashCode = Response.GetHashCode(); - hashCode = (hashCode * 397) ^ (SortedItems?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ SortedItems.GetHashCode(); return hashCode; } } - - public static bool operator ==(VirtualChangeSet left, VirtualChangeSet right) - { - return Equals(left, right); - } - - public static bool operator !=(VirtualChangeSet left, VirtualChangeSet right) - { - return !Equals(left, right); - } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/VirtualRequest.cs b/src/DynamicData/Cache/VirtualRequest.cs index 40759031a..79ab5e1d1 100644 --- a/src/DynamicData/Cache/VirtualRequest.cs +++ b/src/DynamicData/Cache/VirtualRequest.cs @@ -1,21 +1,22 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// A request object for virtualisation + /// A request object for virtualisation. /// public class VirtualRequest : IEquatable, IVirtualRequest { /// - /// The default request value + /// The default request value. /// - public static readonly VirtualRequest Default = new VirtualRequest(0, 25); + public static readonly VirtualRequest Default = new(0, 25); /// /// Initializes a new instance of the class. @@ -36,20 +37,50 @@ public VirtualRequest() } /// - /// The maximumn number of items to return + /// Gets the start index size comparer. + /// + /// + /// The start index size comparer. + /// + public static IEqualityComparer StartIndexSizeComparer { get; } = new StartIndexSizeEqualityComparer(); + + /// + /// Gets the maximum number of items to return. /// public int Size { get; } = 25; /// - /// The first index in the virualised list + /// Gets the first index in the virualised list. /// public int StartIndex { get; } - #region Equality members + /// + public bool Equals(IVirtualRequest? other) + { + return StartIndexSizeComparer.Equals(this, other); + } + + /// + public override bool Equals(object? obj) + { + return obj is IVirtualRequest item && Equals(item); + } + + /// + public override int GetHashCode() + { + unchecked + { + return (StartIndex * 397) ^ Size; + } + } - private sealed class StartIndexSizeEqualityComparer : IEqualityComparer + /// + public override string ToString() => $"StartIndex: {StartIndex}, Size: {Size}"; + + private sealed class StartIndexSizeEqualityComparer : IEqualityComparer { - public bool Equals(IVirtualRequest x, IVirtualRequest y) + public bool Equals(IVirtualRequest? x, IVirtualRequest? y) { if (ReferenceEquals(x, y)) { @@ -74,47 +105,18 @@ public bool Equals(IVirtualRequest x, IVirtualRequest y) return x.StartIndex == y.StartIndex && x.Size == y.Size; } - public int GetHashCode(IVirtualRequest obj) + public int GetHashCode(IVirtualRequest? obj) { + if (obj is null) + { + return 0; + } + unchecked { return (obj.StartIndex * 397) ^ obj.Size; } } } - - /// - /// Gets the start index size comparer. - /// - /// - /// The start index size comparer. - /// - public static IEqualityComparer StartIndexSizeComparer { get; } = new StartIndexSizeEqualityComparer(); - - /// - public bool Equals(IVirtualRequest other) - { - return StartIndexSizeComparer.Equals(this, other); - } - - /// - public override bool Equals(object obj) - { - return Equals((IVirtualRequest)obj); - } - - /// - public override int GetHashCode() - { - unchecked - { - return (StartIndex * 397) ^ Size; - } - } - - #endregion - - /// - public override string ToString() => $"StartIndex: {StartIndex}, Size: {Size}"; } -} +} \ No newline at end of file diff --git a/src/DynamicData/Cache/VirtualResponse.cs b/src/DynamicData/Cache/VirtualResponse.cs index 317ba2ea7..425e51c44 100644 --- a/src/DynamicData/Cache/VirtualResponse.cs +++ b/src/DynamicData/Cache/VirtualResponse.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,7 +9,7 @@ namespace DynamicData { /// - /// Defines values used to virtualise the result set + /// Defines values used to virtualise the result set. /// internal sealed class VirtualResponse : IEquatable, IVirtualResponse { @@ -20,23 +20,23 @@ public VirtualResponse(int size, int startIndex, int totalSize) TotalSize = totalSize; } + public static IEqualityComparer DefaultComparer { get; } = new TotalSizeStartIndexSizeEqualityComparer(); + /// - /// The requested size of the virtualised data + /// Gets the requested size of the virtualised data. /// public int Size { get; } /// - /// The starting index + /// Gets the starting index. /// public int StartIndex { get; } /// - /// Gets the total size of the underlying cache + /// Gets the total size of the underlying cache. /// public int TotalSize { get; } - #region Equality members - /// /// Indicates whether the current object is equal to another object of the same type. /// @@ -44,28 +44,28 @@ public VirtualResponse(int size, int startIndex, int totalSize) /// true if the current object is equal to the parameter; otherwise, false. /// /// An object to compare with this object. - public bool Equals(IVirtualResponse other) + public bool Equals(IVirtualResponse? other) { - return STotalSizeStartIndexSizeComparerInstance.Equals(this, other); + return DefaultComparer.Equals(this, other); } /// - /// Determines whether the specified is equal to the current . + /// Determines whether the specified is equal to the current . /// /// /// true if the specified object is equal to the current object; otherwise, false. /// /// The object to compare with the current object. - public override bool Equals(object obj) + public override bool Equals(object? obj) { - return Equals((IVirtualResponse)obj); + return obj is IVirtualResponse item && Equals(item); } /// /// Serves as a hash function for a particular type. /// /// - /// A hash code for the current . + /// A hash code for the current . /// public override int GetHashCode() { @@ -78,13 +78,20 @@ public override int GetHashCode() } } - #endregion - - #region TotalSizeStartIndexSizeEqualityComparer + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + return $"Size: {Size}, StartIndex: {StartIndex}, TotalSize: {TotalSize}"; + } - private sealed class TotalSizeStartIndexSizeEqualityComparer : IEqualityComparer + private sealed class TotalSizeStartIndexSizeEqualityComparer : IEqualityComparer { - public bool Equals(IVirtualResponse x, IVirtualResponse y) + public bool Equals(IVirtualResponse? x, IVirtualResponse? y) { if (ReferenceEquals(x, y)) { @@ -109,8 +116,13 @@ public bool Equals(IVirtualResponse x, IVirtualResponse y) return x.TotalSize == y.TotalSize && x.StartIndex == y.StartIndex && x.Size == y.Size; } - public int GetHashCode(IVirtualResponse obj) + public int GetHashCode(IVirtualResponse? obj) { + if (obj is null) + { + return 0; + } + unchecked { int hashCode = obj.TotalSize; @@ -120,22 +132,5 @@ public int GetHashCode(IVirtualResponse obj) } } } - - private static readonly IEqualityComparer STotalSizeStartIndexSizeComparerInstance = new TotalSizeStartIndexSizeEqualityComparer(); - - public static IEqualityComparer DefaultComparer => STotalSizeStartIndexSizeComparerInstance; - - #endregion - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString() - { - return $"Size: {Size}, StartIndex: {StartIndex}, TotalSize: {TotalSize}"; - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Constants.cs b/src/DynamicData/Constants.cs new file mode 100644 index 000000000..42d91914e --- /dev/null +++ b/src/DynamicData/Constants.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2011-2020 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 +{ + internal static class Constants + { + public const string EvaluateIsDead = "Use Refresh: Same thing but better semantics"; + } +} \ No newline at end of file diff --git a/src/DynamicData/Diagnostics/ChangeStatistics.cs b/src/DynamicData/Diagnostics/ChangeStatistics.cs index 73ed69fea..80dbf21be 100644 --- a/src/DynamicData/Diagnostics/ChangeStatistics.cs +++ b/src/DynamicData/Diagnostics/ChangeStatistics.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,12 +7,12 @@ namespace DynamicData.Diagnostics { /// - /// Object used to capture accumulated changes + /// Object used to capture accumulated changes. /// public class ChangeStatistics : IEquatable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public ChangeStatistics() { @@ -20,8 +20,15 @@ public ChangeStatistics() } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The index of the change. + /// The number of additions. + /// The number of updates. + /// The number of removals. + /// The number of refreshes. + /// The number of moves. + /// The new count. public ChangeStatistics(int index, int adds, int updates, int removes, int refreshes, int moves, int count) { Index = index; @@ -42,65 +49,85 @@ public ChangeStatistics(int index, int adds, int updates, int removes, int refre public int Adds { get; } /// - /// Gets the updates. + /// Gets the count. /// /// - /// The updates. + /// The count. /// - public int Updates { get; } + public int Count { get; } /// - /// Gets the removes. + /// Gets the index. /// /// - /// The removes. + /// The index. /// - public int Removes { get; } + public int Index { get; } /// - /// Gets the refreshes. + /// Gets the last updated. /// /// - /// The refreshes. + /// The last updated. /// - public int Refreshes { get; } + public DateTime LastUpdated { get; } = DateTime.Now; /// - /// Gets the count. + /// Gets the moves. /// /// - /// The count. + /// The moves. /// - public int Count { get; } + public int Moves { get; } /// - /// Gets the index. + /// Gets the refreshes. /// /// - /// The index. + /// The refreshes. /// - public int Index { get; } + public int Refreshes { get; } /// - /// Gets the moves. + /// Gets the removes. /// /// - /// The moves. + /// The removes. /// - public int Moves { get; } + public int Removes { get; } /// - /// Gets the last updated. + /// Gets the updates. /// /// - /// The last updated. + /// The updates. /// - public DateTime LastUpdated { get; } = DateTime.Now; + public int Updates { get; } + + /// + /// Checks to see if both sides are equal. + /// + /// The left side to compare. + /// The right side to compare. + /// If the two sides are equal. + public static bool operator ==(ChangeStatistics left, ChangeStatistics right) + { + return Equals(left, right); + } - #region Equality members + /// + /// Checks to see if both sides are not equal. + /// + /// The left side to compare. + /// The right side to compare. + /// If the two sides are not equal. + public static bool operator !=(ChangeStatistics left, ChangeStatistics right) + { + return !Equals(left, right); + } /// - public bool Equals(ChangeStatistics other) + public bool Equals(ChangeStatistics? other) { if (ReferenceEquals(null, other)) { @@ -116,24 +143,9 @@ public bool Equals(ChangeStatistics other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((ChangeStatistics)obj); + return obj is ChangeStatistics change && Equals(change); } /// @@ -153,26 +165,10 @@ public override int GetHashCode() } } -#pragma warning disable 1591 - - public static bool operator ==(ChangeStatistics left, ChangeStatistics right) - - { - return Equals(left, right); - } - - public static bool operator !=(ChangeStatistics left, ChangeStatistics right) - { - return !Equals(left, right); - } - - #endregion - /// public override string ToString() { return $"CurrentIndex: {Index}, Adds: {Adds}, Updates: {Updates}, Removes: {Removes}, Refreshes: {Refreshes}, Count: {Count}, Timestamp: {LastUpdated}"; } - } -} +} \ No newline at end of file diff --git a/src/DynamicData/Diagnostics/ChangeSummary.cs b/src/DynamicData/Diagnostics/ChangeSummary.cs index 8ca1a950d..b53c6face 100644 --- a/src/DynamicData/Diagnostics/ChangeSummary.cs +++ b/src/DynamicData/Diagnostics/ChangeSummary.cs @@ -1,24 +1,27 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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.Diagnostics { /// - /// Accumulates change statics + /// Accumulates change statics. /// public class ChangeSummary { - private readonly int _index; - /// - /// An empty instance of change summary + /// An empty instance of change summary. /// - public static readonly ChangeSummary Empty = new ChangeSummary(); + public static readonly ChangeSummary Empty = new(); + + private readonly int _index; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The index of the change. + /// The latest statistics. + /// The overall statistics. public ChangeSummary(int index, ChangeStatistics latest, ChangeStatistics overall) { Latest = latest; @@ -27,7 +30,7 @@ public ChangeSummary(int index, ChangeStatistics latest, ChangeStatistics overal } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// private ChangeSummary() { @@ -37,7 +40,7 @@ private ChangeSummary() } /// - /// Gets the latest change + /// Gets the latest change. /// /// /// The latest. @@ -45,22 +48,15 @@ private ChangeSummary() public ChangeStatistics Latest { get; } /// - /// Gets the overall change count + /// Gets the overall change count. /// /// /// The overall. /// public ChangeStatistics Overall { get; } - #region Equality members - - private bool Equals(ChangeSummary other) - { - return _index == other._index && Equals(Latest, other.Latest) && Equals(Overall, other.Overall); - } - /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -72,12 +68,7 @@ public override bool Equals(object obj) return true; } - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((ChangeSummary)obj); + return obj is ChangeSummary change && Equals(change); } /// @@ -86,15 +77,18 @@ public override int GetHashCode() unchecked { int hashCode = _index; - hashCode = (hashCode * 397) ^ (Latest?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Overall?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ Latest.GetHashCode(); + hashCode = (hashCode * 397) ^ Overall.GetHashCode(); return hashCode; } } - #endregion - /// public override string ToString() => $"CurrentIndex: {_index}, Latest Count: {Latest.Count}, Overall Count: {Overall.Count}"; + + private bool Equals(ChangeSummary other) + { + return _index == other._index && Equals(Latest, other.Latest) && Equals(Overall, other.Overall); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Diagnostics/DiagnosticOperators.cs b/src/DynamicData/Diagnostics/DiagnosticOperators.cs index c0d749caf..1a045354a 100644 --- a/src/DynamicData/Diagnostics/DiagnosticOperators.cs +++ b/src/DynamicData/Diagnostics/DiagnosticOperators.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,68 +8,73 @@ namespace DynamicData.Diagnostics { /// - /// Extensions for diagnostics + /// Extensions for diagnostics. /// public static class DiagnosticOperators { /// - /// Accumulates update statistics + /// Accumulates update statistics. /// /// The type of the source. /// The type of the key. /// The source. - /// - /// source + /// An observable which emits the change summary. + /// source. public static IObservable CollectUpdateStats(this IObservable> source) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Scan(ChangeSummary.Empty, (seed, next) => - { - int index = seed.Overall.Index + 1; - int adds = seed.Overall.Adds + next.Adds; - int updates = seed.Overall.Updates + next.Updates; - int removes = seed.Overall.Removes + next.Removes; - int evaluates = seed.Overall.Refreshes + next.Refreshes; - int moves = seed.Overall.Moves + next.Moves; - int total = seed.Overall.Count + next.Count; + return source.Scan( + ChangeSummary.Empty, + (seed, next) => + { + int index = seed.Overall.Index + 1; + int adds = seed.Overall.Adds + next.Adds; + int updates = seed.Overall.Updates + next.Updates; + int removes = seed.Overall.Removes + next.Removes; + int evaluates = seed.Overall.Refreshes + next.Refreshes; + int moves = seed.Overall.Moves + next.Moves; + int total = seed.Overall.Count + next.Count; - var latest = new ChangeStatistics(index, next.Adds, next.Updates, next.Removes, next.Refreshes, next.Moves, next.Count); - var overall = new ChangeStatistics(index, adds, updates, removes, evaluates, moves, total); - return new ChangeSummary(index, latest, overall); - }); + var latest = new ChangeStatistics(index, next.Adds, next.Updates, next.Removes, next.Refreshes, next.Moves, next.Count); + var overall = new ChangeStatistics(index, adds, updates, removes, evaluates, moves, total); + return new ChangeSummary(index, latest, overall); + }); } /// - /// Accumulates update statistics + /// Accumulates update statistics. /// /// The type of the source. /// The source. - /// - /// source + /// An observable which emits the change summary. + /// source. public static IObservable CollectUpdateStats(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Scan(ChangeSummary.Empty, (seed, next) => - { - int index = seed.Overall.Index + 1; - int adds = seed.Overall.Adds + next.Adds; - int updates = seed.Overall.Updates + next.Replaced; - int removes = seed.Overall.Removes + next.Removes; - int moves = seed.Overall.Moves + next.Moves; - int total = seed.Overall.Count + next.Count; + return source.Scan( + ChangeSummary.Empty, + (seed, next) => + { + int index = seed.Overall.Index + 1; + int adds = seed.Overall.Adds + next.Adds; + int updates = seed.Overall.Updates + next.Replaced; + int removes = seed.Overall.Removes + next.Removes; + int moves = seed.Overall.Moves + next.Moves; + int total = seed.Overall.Count + next.Count; - var latest = new ChangeStatistics(index, next.Adds, next.Replaced, next.Removes, 0, next.Moves, next.Count); - var overall = new ChangeStatistics(index, adds, updates, removes, 0, moves, total); - return new ChangeSummary(index, latest, overall); - }); + var latest = new ChangeStatistics(index, next.Adds, next.Replaced, next.Removes, 0, next.Moves, next.Count); + var overall = new ChangeStatistics(index, adds, updates, removes, 0, moves, total); + return new ChangeSummary(index, latest, overall); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/DynamicData.csproj b/src/DynamicData/DynamicData.csproj index 5b3ffc0d4..26c63bc83 100644 --- a/src/DynamicData/DynamicData.csproj +++ b/src/DynamicData/DynamicData.csproj @@ -1,12 +1,14 @@  - netstandard2.0;net461;uap10.0.16299 + netstandard2.0;net461;uap10.0.16299;net5.0 true true + enable + latest - + diff --git a/src/DynamicData/DynamicData.csproj.DotSettings b/src/DynamicData/DynamicData.csproj.DotSettings index 96331d1ce..2458fb68e 100644 --- a/src/DynamicData/DynamicData.csproj.DotSettings +++ b/src/DynamicData/DynamicData.csproj.DotSettings @@ -1,2 +1,16 @@  - CSharp72 \ No newline at end of file + False + False + False + False + True + True + False + False + False + False + True + True + True + True + True \ No newline at end of file diff --git a/src/DynamicData/EnumerableEx.cs b/src/DynamicData/EnumerableEx.cs index dd68de1fa..7a544c4a7 100644 --- a/src/DynamicData/EnumerableEx.cs +++ b/src/DynamicData/EnumerableEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,94 +7,84 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData { /// - /// Extensions for dynamic data + /// Extensions for dynamic data. /// - [PublicAPI] public static class EnumerableEx { - #region Populate changeset from standard enumerable - /// - /// Converts the enumerable to an observable changeset. + /// Converts the enumerable to an observable change set. /// Generates a snapshot in time based of enumerable. /// /// The type of the object. /// The type of the key. /// The source. /// The key selector. - /// Optionally emmit an OnComplete - /// An observable changeset + /// Optionally emit an OnComplete. + /// An observable change set. /// source /// or - /// keySelector - public static IObservable> AsObservableChangeSet(this IEnumerable source, - Func keySelector, - bool completable = false) + /// keySelector. + public static IObservable> AsObservableChangeSet(this IEnumerable source, Func keySelector, bool completable = false) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return Observable.Create>(obs => - { - var changes = source.Select(x => new Change(ChangeReason.Add, keySelector(x), x)); - var changeSet = new ChangeSet(changes); - obs.OnNext(changeSet); - if (completable) - { - obs.OnCompleted(); - } + return Observable.Create>( + obs => + { + var changes = source.Select(x => new Change(ChangeReason.Add, keySelector(x), x)); + var changeSet = new ChangeSet(changes); + obs.OnNext(changeSet); + if (completable) + { + obs.OnCompleted(); + } - return Disposable.Empty; - }); + return Disposable.Empty; + }); } - #endregion - - #region Populate change set from standard enumerable - /// - /// Converts the enumerable to an observable changeset. + /// Converts the enumerable to an observable change set. /// Generates a snapshot in time based of enumerable. /// /// The type of the object. /// The source. - /// Optionally emmit an OnComplete - /// An observable change set - /// source - public static IObservable> AsObservableChangeSet(this IEnumerable source, - bool completable = false) + /// Optionally emit an OnComplete. + /// An observable change set. + /// source. + public static IObservable> AsObservableChangeSet(this IEnumerable source, bool completable = false) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return Observable.Create>(obs => - { - var changes = source.Select(x => new Change(ListChangeReason.Add, x)); - var changeSet = new ChangeSet(changes); - obs.OnNext(changeSet); - if (completable) - { - obs.OnCompleted(); - } + return Observable.Create>( + obs => + { + var changes = source.Select(x => new Change(ListChangeReason.Add, x)); + var changeSet = new ChangeSet(changes); + obs.OnNext(changeSet); + if (completable) + { + obs.OnCompleted(); + } - return Disposable.Empty; - }); + return Disposable.Empty; + }); } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/Experimental/ExperimentalEx.cs b/src/DynamicData/Experimental/ExperimentalEx.cs index 69b0a5eff..3823d5728 100644 --- a/src/DynamicData/Experimental/ExperimentalEx.cs +++ b/src/DynamicData/Experimental/ExperimentalEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,22 +8,23 @@ namespace DynamicData.Experimental { /// - /// Experimental operator extensions + /// Experimental operator extensions. /// public static class ExperimentalEx { /// - /// Wraps the source cache, optimising it for watching individual updates + /// Wraps the source cache, optimising it for watching individual updates. /// /// The type of the object. /// The type of the key. /// The source. /// The scheduler. - /// - /// source - public static IWatcher AsWatcher(this IObservable> source, IScheduler scheduler = null) + /// The watcher. + /// source. + public static IWatcher AsWatcher(this IObservable> source, IScheduler? scheduler = null) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -31,4 +32,4 @@ public static IWatcher AsWatcher(this IObservable< return new Watcher(source, scheduler ?? Scheduler.Default); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Experimental/ISubjectWithRefCount.cs b/src/DynamicData/Experimental/ISubjectWithRefCount.cs index 5a242406a..7ec3ae6b8 100644 --- a/src/DynamicData/Experimental/ISubjectWithRefCount.cs +++ b/src/DynamicData/Experimental/ISubjectWithRefCount.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,12 +6,16 @@ namespace DynamicData.Experimental { + /// + /// A subject which also contains its current reference count. + /// + /// The type of item. internal interface ISubjectWithRefCount : ISubject { - /// number of subscribers. + /// Gets number of subscribers. /// /// The ref count. /// int RefCount { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Experimental/IWatcher.cs b/src/DynamicData/Experimental/IWatcher.cs index 7585fec6c..2a1778d73 100644 --- a/src/DynamicData/Experimental/IWatcher.cs +++ b/src/DynamicData/Experimental/IWatcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,15 +7,18 @@ namespace DynamicData.Experimental { /// - /// A specialisation of the SourceList which is optimised for watching individual items + /// A specialisation of the SourceList which is optimised for watching individual items. /// + /// The type of the object. + /// The type of the key. public interface IWatcher : IDisposable + where TKey : notnull { /// /// Watches updates which match the specified key. /// /// The key. - /// + /// An observable which emits the change. IObservable> Watch(TKey key); } -} +} \ No newline at end of file diff --git a/src/DynamicData/Experimental/SubjectWithRefCount.cs b/src/DynamicData/Experimental/SubjectWithRefCount.cs index 19545d439..e4ea5edf4 100644 --- a/src/DynamicData/Experimental/SubjectWithRefCount.cs +++ b/src/DynamicData/Experimental/SubjectWithRefCount.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,29 +10,36 @@ namespace DynamicData.Experimental { /// - /// A subject with a count of the number of subscribers + /// A subject with a count of the number of subscribers. /// - /// + /// The type of the item. internal class SubjectWithRefCount : ISubjectWithRefCount { private readonly ISubject _subject; + private int _refCount; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public SubjectWithRefCount(ISubject subject = null) + /// The subject to perform reference counting on. + public SubjectWithRefCount(ISubject? subject = null) { _subject = subject ?? new Subject(); } + /// Gets number of subscribers. + /// + /// The ref count. + /// + public int RefCount => _refCount; + /// - /// Provides the observer with new data. + /// Notifies the observer that the provider has finished sending push-based notifications. /// - /// The current notification information. - public void OnNext(T value) + public void OnCompleted() { - _subject.OnNext(value); + _subject.OnCompleted(); } /// @@ -45,11 +52,12 @@ public void OnError(Exception error) } /// - /// Notifies the observer that the provider has finished sending push-based notifications. + /// Provides the observer with new data. /// - public void OnCompleted() + /// The current notification information. + public void OnNext(T value) { - _subject.OnCompleted(); + _subject.OnNext(value); } /// @@ -64,17 +72,12 @@ public IDisposable Subscribe(IObserver observer) Interlocked.Increment(ref _refCount); var subscriber = _subject.Subscribe(observer); - return Disposable.Create(() => - { - Interlocked.Decrement(ref _refCount); - subscriber.Dispose(); - }); + return Disposable.Create( + () => + { + Interlocked.Decrement(ref _refCount); + subscriber.Dispose(); + }); } - - /// number of subscribers. - /// - /// The ref count. - /// - public int RefCount => _refCount; } -} +} \ No newline at end of file diff --git a/src/DynamicData/Experimental/Watcher.cs b/src/DynamicData/Experimental/Watcher.cs index a6364d7e8..bae222a7c 100644 --- a/src/DynamicData/Experimental/Watcher.cs +++ b/src/DynamicData/Experimental/Watcher.cs @@ -1,66 +1,71 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. -#region - using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; -using DynamicData.Kernel; -#endregion +using DynamicData.Kernel; namespace DynamicData.Experimental { internal sealed class Watcher : IWatcher + where TKey : notnull { - private readonly IntermediateCache>, TKey> _subscribers = new IntermediateCache>, TKey>(); + private readonly IDisposable _disposer; + + private readonly object _locker = new(); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed with _cleanUp")] private readonly IObservableCache _source; - private readonly object _locker = new object(); - private readonly IDisposable _disposer; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed with _cleanUp")] + private readonly IntermediateCache>, TKey> _subscribers = new(); public Watcher(IObservable> source, IScheduler scheduler) { _source = source.AsObservableCache(); - var onCompletePublisher = _subscribers.Connect() - .Synchronize(_locker) - .ObserveOn(scheduler) - .SubscribeMany((t, k) => Disposable.Create(t.OnCompleted)) - .Subscribe(); - - var sourceSubscriber = source.Synchronize(_locker).Subscribe(updates => updates.ForEach(update => - { - var subscriber = _subscribers.Lookup(update.Key); - if (subscriber.HasValue) - { - scheduler.Schedule(() => subscriber.Value.OnNext(update)); - } - })); - - _disposer = Disposable.Create(() => - { - onCompletePublisher.Dispose(); - sourceSubscriber.Dispose(); - - _source.Dispose(); - _subscribers.Dispose(); - }); + var onCompletePublisher = _subscribers.Connect().Synchronize(_locker).ObserveOn(scheduler).SubscribeMany((t, _) => Disposable.Create(t.OnCompleted)).Subscribe(); + + var sourceSubscriber = source.Synchronize(_locker).Subscribe( + updates => updates.ForEach( + update => + { + var subscriber = _subscribers.Lookup(update.Key); + if (subscriber.HasValue) + { + scheduler.Schedule(() => subscriber.Value.OnNext(update)); + } + })); + + _disposer = Disposable.Create( + () => + { + onCompletePublisher.Dispose(); + sourceSubscriber.Dispose(); + + _source.Dispose(); + _subscribers.Dispose(); + }); + } + + public void Dispose() + { + _disposer.Dispose(); } public IObservable> Watch(TKey key) { - return Observable.Create> - ( - observer => + return Observable.Create>( + observer => { lock (_locker) { - //Create or find the existing subscribers + // Create or find the existing subscribers var existing = _subscribers.Lookup(key); SubjectWithRefCount> subject; if (existing.HasValue) @@ -81,29 +86,25 @@ public IObservable> Watch(TKey key) _subscribers.Edit(updater => updater.AddOrUpdate(subject, key)); } - //set up subscription + // set up subscription var subscriber = subject.SubscribeSafe(observer); - return Disposable.Create(() => - { - //lock to ensure no race condition where the same key could be subscribed - //to whilst disposal is taking place - lock (_locker) - { - subscriber.Dispose(); - if (subject.RefCount == 0) + return Disposable.Create( + () => { - _subscribers.Remove(key); - } - } - }); + // lock to ensure no race condition where the same key could be subscribed + // to whilst disposal is taking place + lock (_locker) + { + subscriber.Dispose(); + if (subject.RefCount == 0) + { + _subscribers.Remove(key); + } + } + }); } }); } - - public void Dispose() - { - _disposer.Dispose(); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/IChangeSet.cs b/src/DynamicData/IChangeSet.cs index 2cd127f9c..2db3aae1b 100644 --- a/src/DynamicData/IChangeSet.cs +++ b/src/DynamicData/IChangeSet.cs @@ -1,45 +1,45 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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 { /// - /// Base interface representing a set of changes + /// Base interface representing a set of changes. /// public interface IChangeSet { /// - /// Gets the number of additions + /// Gets the number of additions. /// int Adds { get; } /// - /// Gets the number of removes + /// Gets or sets the capacity of the change set. /// - int Removes { get; } + /// + /// The capacity. + /// + int Capacity { get; set; } /// - /// The number of refreshes + /// Gets the total update count. /// - int Refreshes { get; } + int Count { get; } /// - /// Gets the number of moves + /// Gets the number of moves. /// int Moves { get; } /// - /// The total update count + /// Gets the number of refreshes. /// - int Count { get; } + int Refreshes { get; } /// - /// Gets or sets the capacity of the change set + /// Gets the number of removes. /// - /// - /// The capacity. - /// - int Capacity { get; set; } + int Removes { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/ConnectionStatus.cs b/src/DynamicData/Kernel/ConnectionStatus.cs index 25adc598a..9e044df7e 100644 --- a/src/DynamicData/Kernel/ConnectionStatus.cs +++ b/src/DynamicData/Kernel/ConnectionStatus.cs @@ -1,11 +1,11 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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.Kernel { /// - /// Connectable cache status + /// Connectable cache status. /// public enum ConnectionStatus { @@ -30,4 +30,4 @@ public enum ConnectionStatus /// Completed = 3 } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/DoubleCheck.cs b/src/DynamicData/Kernel/DoubleCheck.cs deleted file mode 100644 index 9118765c7..000000000 --- a/src/DynamicData/Kernel/DoubleCheck.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2011-2019 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. - -using System; - -namespace DynamicData.Kernel -{ - /// - /// Very simple, primitive yet light weight lazy loader - /// - /// - public sealed class DoubleCheck - where T : class - { - private readonly Func _factory; - private readonly object _locker = new object(); - private volatile T _value; - - /// - /// Initializes a new instance of the class. - /// - /// The factory. - /// factory - public DoubleCheck(Func factory) - { - _factory = factory ?? throw new ArgumentNullException(nameof(factory)); - } - - /// - /// Gets the value. Factory is execute when first called - /// - public T Value - { - get - { - if (_value != null) - { - return _value; - } - - lock (_locker) - { - if (_value == null) - { - _value = _factory(); - } - } - - return _value; - } - } - } -} diff --git a/src/DynamicData/Kernel/EnumerableEx.cs b/src/DynamicData/Kernel/EnumerableEx.cs index e844b2d3a..0e0498f6e 100644 --- a/src/DynamicData/Kernel/EnumerableEx.cs +++ b/src/DynamicData/Kernel/EnumerableEx.cs @@ -1,28 +1,27 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Linq; -using DynamicData.Annotations; namespace DynamicData.Kernel { /// - /// Enumerable extensions + /// Enumerable extensions. /// public static class EnumerableEx { /// - /// Casts the enumerable to an array if it is already an array. Otherwise call ToArray + /// Casts the enumerable to an array if it is already an array. Otherwise call ToArray. /// - /// + /// The type of the item. /// The source. - /// - public static T[] AsArray([NotNull] this IEnumerable source) + /// The array of items. + public static T[] AsArray(this IEnumerable source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -31,14 +30,14 @@ public static T[] AsArray([NotNull] this IEnumerable source) } /// - /// Casts the enumerable to an array if it is already an array. Otherwise call ToList + /// Casts the enumerable to an array if it is already an array. Otherwise call ToList. /// - /// + /// The type of the item. /// The source. - /// - public static List AsList([NotNull] this IEnumerable source) + /// The list. + public static List AsList(this IEnumerable source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -47,43 +46,37 @@ public static List AsList([NotNull] this IEnumerable source) } /// - /// Returns any duplicated values from the source + /// Returns any duplicated values from the source. /// - /// + /// The type of the item. /// The type of the value. /// The source. /// The value selector. - /// - /// - /// - public static IEnumerable Duplicates([NotNull] this IEnumerable source, - [NotNull] Func valueSelector) + /// The enumerable of items. + public static IEnumerable Duplicates(this IEnumerable source, Func valueSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (valueSelector is null) { throw new ArgumentNullException(nameof(valueSelector)); } - return source.GroupBy(valueSelector) - .Where(group => group.Count() > 1) - .SelectMany(t => t); + return source.GroupBy(valueSelector).Where(group => group.Count() > 1).SelectMany(t => t); } /// /// Finds the index of many items as specified in the secondary enumerable. /// - /// + /// The type of the item. /// The source. - /// The items to find in the source enumerable + /// The items to find in the source enumerable. /// - /// A result as specified by the result selector + /// A result as specified by the result selector. /// - /// public static IEnumerable> IndexOfMany(this IEnumerable source, IEnumerable itemsToFind) { return source.IndexOfMany(itemsToFind, (t, idx) => new ItemWithIndex(t, idx)); @@ -96,42 +89,37 @@ public static IEnumerable> IndexOfMany(this IEnumerable s /// The type of the result. /// The source. /// The items to find. - /// The result selector - /// A result as specified by the result selector - /// - /// - public static IEnumerable IndexOfMany([NotNull] this IEnumerable source, [NotNull] IEnumerable itemsToFind, [NotNull] Func resultSelector) + /// The result selector. + /// A result as specified by the result selector. + public static IEnumerable IndexOfMany(this IEnumerable source, IEnumerable itemsToFind, Func resultSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (itemsToFind == null) + if (itemsToFind is null) { throw new ArgumentNullException(nameof(itemsToFind)); } - if (resultSelector == null) + if (resultSelector is null) { throw new ArgumentNullException(nameof(resultSelector)); } var indexed = source.Select((element, index) => new { Element = element, Index = index }); - return itemsToFind - .Join(indexed, left => left, right => right.Element, (left, right) => right) - .Select(x => resultSelector(x.Element, x.Index)); + return itemsToFind.Join(indexed, left => left, right => right.Element, (_, right) => right).Select(x => resultSelector(x.Element, x.Index)); } - /// - /// Returns an object with it's current index. - /// - /// - /// The source. - /// - internal static IEnumerable> WithIndex(this IEnumerable source) + internal static IEnumerable EmptyIfNull(this IEnumerable? source) { - return source.Select((item, index) => new ItemWithIndex(item, index)); + return source ?? Enumerable.Empty(); + } + + internal static IEnumerable EnumerateOne(this T source) + { + yield return source; } internal static void ForEach(this IEnumerable source, Action action) @@ -152,23 +140,23 @@ internal static void ForEach(this IEnumerable source, Action EmptyIfNull(this IEnumerable source) +#if !WINDOWS_UWP + internal static HashSet ToHashSet(this IEnumerable source) { - return source ?? Enumerable.Empty(); + return new(source); } - #if (!WINDOWS_UWP) +#endif - internal static HashSet ToHashSet(this IEnumerable source) - { - return new HashSet(source); - } - - #endif - - internal static IEnumerable EnumerateOne(this T source) + /// + /// Returns an object with it's current index. + /// + /// The type of the item. + /// The source. + /// The enumerable of items with their indexes. + internal static IEnumerable> WithIndex(this IEnumerable source) { - yield return source; + return source.Select((item, index) => new ItemWithIndex(item, index)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/EnumerableIList.cs b/src/DynamicData/Kernel/EnumerableIList.cs index 4aa96e03a..7bbb7d54d 100644 --- a/src/DynamicData/Kernel/EnumerableIList.cs +++ b/src/DynamicData/Kernel/EnumerableIList.cs @@ -1,51 +1,15 @@ -// Copyright (c) Ben A Adams. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root (on link below) for license information. - -//Lifted from here https://github.com/benaadams/Ben.Enumerable. Many thanks to the genius of the man. +// Copyright (c) 2011-2020 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. using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +// Lifted from here https://github.com/benaadams/Ben.Enumerable. Many thanks to the genius of the man. namespace DynamicData.Kernel { - - internal static class EnumerableIList - { - - public static EnumerableIList Create(IList list) => new EnumerableIList(list); - public static EnumerableIList> Create(IChangeSet changeset) => Create((IList>)changeset); - } - - internal struct EnumeratorIList : IEnumerator - { - private readonly IList _list; - private int _index; - - public EnumeratorIList(IList list) - { - _index = -1; - _list = list; - } - - public T Current => _list[_index]; - - public bool MoveNext() - { - _index++; - - return _index < (_list?.Count ?? 0); - } - - public void Dispose() { } - object IEnumerator.Current => Current; - public void Reset() => _index = -1; - } - - internal interface IEnumerableIList : IEnumerable - { - new EnumeratorIList GetEnumerator(); - } - + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Same class name, different generics.")] internal readonly struct EnumerableIList : IEnumerableIList, IList { private readonly IList _list; @@ -55,18 +19,7 @@ public EnumerableIList(IList list) _list = list; } - public EnumeratorIList GetEnumerator() => new EnumeratorIList(_list); - - public static implicit operator EnumerableIList(List list) => new EnumerableIList(list); - - public static implicit operator EnumerableIList(T[] array) => new EnumerableIList(array); - - public static EnumerableIList Empty; - - // IList pass through - - /// - public T this[int index] { get => _list[index]; set => _list[index] = value; } + public static EnumerableIList Empty { get; } /// public int Count => _list.Count; @@ -74,6 +27,19 @@ public EnumerableIList(IList list) /// public bool IsReadOnly => _list.IsReadOnly; + /// + public T this[int index] + { + get => _list[index]; + set => _list[index] = value; + } + + public static implicit operator EnumerableIList(List list) => new(list); + + public static implicit operator EnumerableIList(T[] array) => new(array); + + public EnumeratorIList GetEnumerator() => new(_list); + /// public void Add(T item) => _list.Add(item); @@ -102,4 +68,13 @@ public EnumerableIList(IList list) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + internal static class EnumerableIList + { + public static EnumerableIList Create(IList list) => new(list); + + public static EnumerableIList> Create(IChangeSet changeSet) + where TKey : notnull => + Create((IList>)changeSet); + } } \ No newline at end of file diff --git a/src/DynamicData/Kernel/EnumeratorIList.cs b/src/DynamicData/Kernel/EnumeratorIList.cs new file mode 100644 index 000000000..6aeb2b810 --- /dev/null +++ b/src/DynamicData/Kernel/EnumeratorIList.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2011-2020 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. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +// Lifted from here https://github.com/benaadams/Ben.Enumerable. Many thanks to the genius of the man. +namespace DynamicData.Kernel +{ + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Same class name, different generics.")] + internal struct EnumeratorIList : IEnumerator + { + private readonly IList _list; + + private int _index; + + public EnumeratorIList(IList list) + { + _index = -1; + _list = list; + } + + public T Current => _list[_index]; + + object? IEnumerator.Current => Current; + + public bool MoveNext() + { + _index++; + + return _index < _list.Count; + } + + public void Dispose() + { + } + + public void Reset() => _index = -1; + } +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/Error.cs b/src/DynamicData/Kernel/Error.cs index 0f90830ad..073d8c201 100644 --- a/src/DynamicData/Kernel/Error.cs +++ b/src/DynamicData/Kernel/Error.cs @@ -1,30 +1,40 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + #pragma warning disable 1591 namespace DynamicData.Kernel { /// - /// An error container used to report errors from within dynamic data operators + /// An error container used to report errors from within dynamic data operators. /// /// The type of the object. /// The type of the key. public sealed class Error : IKeyValue, IEquatable> + where TKey : notnull { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public Error(Exception exception, TObject value, TKey key) + /// The exception that caused the error. + /// The value for the error. + /// The key for the error. + public Error(Exception? exception, TObject value, TKey key) { Exception = exception; Value = value; Key = key; } + /// + /// Gets the exception. + /// + public Exception? Exception { get; } + /// /// Gets the key. /// @@ -35,13 +45,6 @@ public Error(Exception exception, TObject value, TKey key) /// public TObject Value { get; } - /// - /// The exception. - /// - public Exception Exception { get; } - - #region Equality members - public static bool operator ==(Error left, Error right) { return Equals(left, right); @@ -53,7 +56,7 @@ public Error(Exception exception, TObject value, TKey key) } /// - public bool Equals(Error other) + public bool Equals(Error? other) { if (ReferenceEquals(null, other)) { @@ -69,7 +72,7 @@ public bool Equals(Error other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -81,7 +84,7 @@ public override bool Equals(object obj) return true; } - return obj is Error && Equals((Error)obj); + return obj is Error error && Equals(error); } /// @@ -90,18 +93,16 @@ public override int GetHashCode() unchecked { int hashCode = EqualityComparer.Default.GetHashCode(Key); - hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Value); - hashCode = (hashCode * 397) ^ (Exception != null ? Exception.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Value is null ? 0 : EqualityComparer.Default.GetHashCode(Value)); + hashCode = (hashCode * 397) ^ (Exception is not null ? Exception.GetHashCode() : 0); return hashCode; } } - #endregion - /// public override string ToString() { return $"Key: {Key}, Value: {Value}, Exception: {Exception}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/IEnumerableIList.cs b/src/DynamicData/Kernel/IEnumerableIList.cs new file mode 100644 index 000000000..ac88d0066 --- /dev/null +++ b/src/DynamicData/Kernel/IEnumerableIList.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2011-2020 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. + +using System.Collections.Generic; + +// Lifted from here https://github.com/benaadams/Ben.Enumerable. Many thanks to the genius of the man. +namespace DynamicData.Kernel +{ + /// + /// A enumerable that also contains the enumerable list. + /// + /// The type of items. + internal interface IEnumerableIList : IEnumerable + { + /// + /// Gets the enumerator. + /// + /// The enumerator. + new EnumeratorIList GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/ISupportsCapacity.cs b/src/DynamicData/Kernel/ISupportsCapacity.cs new file mode 100644 index 000000000..05d9f54db --- /dev/null +++ b/src/DynamicData/Kernel/ISupportsCapacity.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2011-2020 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.Kernel +{ + /// + /// A collection type that supports a capacity. + /// + internal interface ISupportsCapacity + { + /// + /// Gets or sets the capacity. + /// + int Capacity { get; set; } + + /// + /// Gets the number of items. + /// + int Count { get; } + } +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/ISupportsCapcity.cs b/src/DynamicData/Kernel/ISupportsCapcity.cs deleted file mode 100644 index 5b72200e0..000000000 --- a/src/DynamicData/Kernel/ISupportsCapcity.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2011-2019 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.Kernel -{ - internal interface ISupportsCapcity - { - int Capacity { get; set; } - int Count { get; } - } -} \ No newline at end of file diff --git a/src/DynamicData/Kernel/InternalEx.cs b/src/DynamicData/Kernel/InternalEx.cs index 354b79494..a79899253 100644 --- a/src/DynamicData/Kernel/InternalEx.cs +++ b/src/DynamicData/Kernel/InternalEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,7 +7,6 @@ using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; -using System.Threading.Tasks; namespace DynamicData.Kernel { @@ -21,59 +20,52 @@ public static class InternalEx /// /// /// With big thanks. I took this from - /// http://social.msdn.microsoft.com/Forums/en-US/af43b14e-fb00-42d4-8fb1-5c45862f7796/recursive-async-web-requestresponse-what-is-best-practice-3rd-try + /// http://social.msdn.microsoft.com/Forums/en-US/af43b14e-fb00-42d4-8fb1-5c45862f7796/recursive-async-web-requestresponse-what-is-best-practice-3rd-try. /// /// The type of the source. /// The type of the exception. /// The source. /// The back off strategy. - /// + /// An observable which will emit the value. public static IObservable RetryWithBackOff(this IObservable source, Func backOffStrategy) where TException : Exception { - IObservable Retry(int failureCount) => source.Catch(error => - { - TimeSpan? delay = backOffStrategy(error, failureCount); - if (!delay.HasValue) - { - return Observable.Throw(error); - } + IObservable Retry(int failureCount) => + source.Catch( + error => + { + TimeSpan? delay = backOffStrategy(error, failureCount); + if (!delay.HasValue) + { + return Observable.Throw(error); + } - return Observable.Timer(delay.Value).SelectMany(Retry(failureCount + 1)); - }); + return Observable.Timer(delay.Value).SelectMany(Retry(failureCount + 1)); + }); return Retry(0); } - internal static IObservable ToUnit(this IObservable source) - { - return source.Select(_ => Unit.Default); - } - - internal static void OnNext(this ISubject source) - { - source.OnNext(Unit.Default); - } - - /// /// Schedules a recurring action. /// /// /// I took this from - /// http://www.zerobugbuild.com/?p=259 + /// http://www.zerobugbuild.com/?p=259. /// /// The scheduler. /// The interval. /// The action. - /// + /// A disposable that will stop the schedule. public static IDisposable ScheduleRecurringAction(this IScheduler scheduler, TimeSpan interval, Action action) { - return scheduler.Schedule(interval, scheduleNext => - { - action(); - scheduleNext(interval); - }); + return scheduler.Schedule( + interval, + scheduleNext => + { + action(); + scheduleNext(interval); + }); } /// @@ -84,25 +76,32 @@ public static IDisposable ScheduleRecurringAction(this IScheduler scheduler, Tim /// /// http://www.zerobugbuild.com/?p=259 /// - /// and adapted it to receive + /// and adapted it to receive. /// /// The scheduler. /// The interval. /// The action. - /// + /// A disposable that will stop the schedule. public static IDisposable ScheduleRecurringAction(this IScheduler scheduler, Func interval, Action action) { - if (interval == null) + if (interval is null) { throw new ArgumentNullException(nameof(interval)); } - return scheduler.Schedule(interval(), scheduleNext => - { - action(); - var next = interval(); - scheduleNext(next); - }); + return scheduler.Schedule( + interval(), + scheduleNext => + { + action(); + var next = interval(); + scheduleNext(next); + }); + } + + internal static void OnNext(this ISubject source) + { + source.OnNext(Unit.Default); } internal static void Swap(ref TSwap t1, ref TSwap t2) @@ -111,5 +110,10 @@ internal static void Swap(ref TSwap t1, ref TSwap t2) t1 = t2; t2 = temp; } + + internal static IObservable ToUnit(this IObservable source) + { + return source.Select(_ => Unit.Default); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/ItemWithIndex.cs b/src/DynamicData/Kernel/ItemWithIndex.cs index c15fa62be..e1690ca2f 100644 --- a/src/DynamicData/Kernel/ItemWithIndex.cs +++ b/src/DynamicData/Kernel/ItemWithIndex.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,11 +8,23 @@ namespace DynamicData.Kernel { /// - /// Container for an item and it's index from a list + /// Container for an item and it's index from a list. /// - /// + /// The type of the item. public readonly struct ItemWithIndex : IEquatable> { + /// + /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. + /// + /// The item. + /// The index. + public ItemWithIndex(T item, int index) + { + Item = item; + Index = index; + } + /// /// Gets the item. /// @@ -23,18 +35,23 @@ namespace DynamicData.Kernel /// public int Index { get; } - /// - /// Initializes a new instance of the class. - /// - /// The item. - /// The index. - public ItemWithIndex(T item, int index) + /// Returns a value that indicates whether the values of two objects are equal. + /// The first value to compare. + /// The second value to compare. + /// true if the and parameters have the same value; otherwise, false. + public static bool operator ==(ItemWithIndex left, ItemWithIndex right) { - Item = item; - Index = index; + return left.Equals(right); } - #region Equality + /// Returns a value that indicates whether two objects have different values. + /// The first value to compare. + /// The second value to compare. + /// true if and are not equal; otherwise, false. + public static bool operator !=(ItemWithIndex left, ItemWithIndex right) + { + return !left.Equals(right); + } /// public bool Equals(ItemWithIndex other) @@ -43,47 +60,27 @@ public bool Equals(ItemWithIndex other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { return false; } - return obj is ItemWithIndex && Equals((ItemWithIndex) obj); + return obj is ItemWithIndex itemWithIndex && Equals(itemWithIndex); } /// Returns the hash code for this instance. /// A 32-bit signed integer that is the hash code for this instance. public override int GetHashCode() { - return EqualityComparer.Default.GetHashCode(Item); - } - - /// Returns a value that indicates whether the values of two objects are equal. - /// The first value to compare. - /// The second value to compare. - /// true if the and parameters have the same value; otherwise, false. - public static bool operator ==(ItemWithIndex left, ItemWithIndex right) - { - return left.Equals(right); - } - - /// Returns a value that indicates whether two objects have different values. - /// The first value to compare. - /// The second value to compare. - /// true if and are not equal; otherwise, false. - public static bool operator !=(ItemWithIndex left, ItemWithIndex right) - { - return !left.Equals(right); + return Item is null ? 0 : EqualityComparer.Default.GetHashCode(Item); } - #endregion - /// public override string ToString() { return $"{Item} ({Index})"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/ItemWithValue.cs b/src/DynamicData/Kernel/ItemWithValue.cs index 1d2673e4e..117183dfb 100644 --- a/src/DynamicData/Kernel/ItemWithValue.cs +++ b/src/DynamicData/Kernel/ItemWithValue.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,23 +8,14 @@ namespace DynamicData.Kernel { /// - /// Container for an item and it's Value from a list + /// Container for an item and it's Value from a list. /// /// The type of the object. /// The type of the value. public readonly struct ItemWithValue : IEquatable> { /// - /// Gets the item. - /// - public TObject Item { get; } - - /// - /// Gets the Value. - /// - public TValue Value { get; } - - /// + /// Initializes a new instance of the struct. /// Initializes a new instance of the class. /// /// The item. @@ -35,33 +26,15 @@ public ItemWithValue(TObject item, TValue value) Value = value; } - #region Equality - - /// - public bool Equals(ItemWithValue other) - { - return EqualityComparer.Default.Equals(Item, other.Item) && EqualityComparer.Default.Equals(Value, other.Value); - } - - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return obj is ItemWithValue && Equals((ItemWithValue) obj); - } + /// + /// Gets the item. + /// + public TObject Item { get; } - /// - public override int GetHashCode() - { - unchecked - { - return (EqualityComparer.Default.GetHashCode(Item) * 397) ^ EqualityComparer.Default.GetHashCode(Value); - } - } + /// + /// Gets the Value. + /// + public TValue Value { get; } /// /// Implements the operator ==. @@ -89,9 +62,33 @@ public override int GetHashCode() return !Equals(left, right); } - #endregion + /// + public bool Equals(ItemWithValue other) + { + return EqualityComparer.Default.Equals(Item, other.Item) && EqualityComparer.Default.Equals(Value, other.Value); + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is ItemWithValue itemWithValue && Equals(itemWithValue); + } + + /// + public override int GetHashCode() + { + unchecked + { + return (Item is null ? 0 : EqualityComparer.Default.GetHashCode(Item) * 397) ^ (Value is null ? 0 : EqualityComparer.Default.GetHashCode(Value)); + } + } /// public override string ToString() => $"{Item} ({Value})"; } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/OptionElse.cs b/src/DynamicData/Kernel/OptionElse.cs index aea879763..1258df183 100644 --- a/src/DynamicData/Kernel/OptionElse.cs +++ b/src/DynamicData/Kernel/OptionElse.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,11 +7,11 @@ namespace DynamicData.Kernel { /// - /// Continuation container used for the else optator on an option object. + /// Continuation container used for the else operator on an option object. /// public sealed class OptionElse { - internal static readonly OptionElse NoAction = new OptionElse(false); + internal static readonly OptionElse NoAction = new(false); private readonly bool _shouldRunAction; @@ -24,10 +24,10 @@ internal OptionElse(bool shouldRunAction = true) /// Invokes the specified action when an option has no value. /// /// The action. - /// action + /// action. public void Else(Action action) { - if (action == null) + if (action is null) { throw new ArgumentNullException(nameof(action)); } @@ -38,4 +38,4 @@ public void Else(Action action) } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/OptionExtensions.cs b/src/DynamicData/Kernel/OptionExtensions.cs index f39216da5..6e65c029a 100644 --- a/src/DynamicData/Kernel/OptionExtensions.cs +++ b/src/DynamicData/Kernel/OptionExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,136 +9,131 @@ namespace DynamicData.Kernel { /// - /// Extensions for optional + /// Extensions for optional. /// public static class OptionExtensions { /// - /// Returns the value if the nullable has a value, otherwise returns the result of the value selector + /// Converts the specified source. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// The default value - /// - public static T ValueOr(this T? source, T defaultValue) - where T : struct + /// The converter. + /// The converted value. + /// converter. + public static Optional Convert(this Optional source, Func converter) { - return source ?? defaultValue; + if (converter is null) + { + throw new ArgumentNullException(nameof(converter)); + } + + return source.HasValue ? converter(source.Value) : Optional.None(); } /// - /// Returns the value if the optional has a value, otherwise returns the result of the value selector + /// Converts the option value if it has a value, otherwise returns the result of the fallback converter. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// The value selector. - /// - /// valueSelector - public static T ValueOr(this Optional source, Func valueSelector) + /// The converter. + /// The fallback converter. + /// The destination value. + /// + /// converter + /// or + /// fallbackConverter. + /// + public static TDestination? ConvertOr(this Optional source, Func converter, Func fallbackConverter) { - if (valueSelector == null) + if (converter is null) { - throw new ArgumentNullException(nameof(valueSelector)); + throw new ArgumentNullException(nameof(converter)); } - return source.HasValue ? source.Value : valueSelector(); + if (fallbackConverter is null) + { + throw new ArgumentNullException(nameof(fallbackConverter)); + } + + return source.HasValue ? converter(source.Value) : fallbackConverter(); } /// - /// Returns the value if the optional has a value, otherwise returns the default value of T + /// Overloads Enumerable.FirstOrDefault() and wraps the result in a Optional + /// &gt;T + /// container. /// - /// + /// The type of the item. /// The source. - /// - public static T ValueOrDefault(this Optional source) + /// The selector. + /// The first value or none. + public static Optional FirstOrOptional(this IEnumerable source, Func selector) { - return source.HasValue ? source.Value : default(T); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + var result = source.FirstOrDefault(selector); + return !Equals(result, null) ? result : Optional.None(); } /// - /// Returns the value if the optional has a value, otherwise throws an exception as specified by the exception generator + /// Invokes the specified action when. /// - /// + /// The type of the item. /// The source. - /// The exception generator. - /// - /// exceptionGenerator - public static T ValueOrThrow(this Optional source, Func exceptionGenerator) + /// The action. + /// The optional else extension. + public static OptionElse IfHasValue(this Optional source, Action action) { - if (exceptionGenerator == null) + if (!source.HasValue || source.Value is null) { - throw new ArgumentNullException(nameof(exceptionGenerator)); + return new OptionElse(); } - if (source.HasValue) + if (action is null) { - return source.Value; + throw new ArgumentNullException(nameof(action)); } - throw exceptionGenerator(); + action(source.Value); + return OptionElse.NoAction; } /// - /// Converts the option value if it has a value, otherwise returns the result of the fallback converter + /// Invokes the specified action when. /// - /// The type of the source. - /// The type of the destination. + /// The type of the item. /// The source. - /// The converter. - /// The fallback converter. - /// - /// - /// converter - /// or - /// fallbackConverter - /// - public static TDestination ConvertOr(this Optional source, Func converter, Func fallbackConverter) + /// The action. + /// The optional else extension. + public static OptionElse IfHasValue(this Optional? source, Action action) { - if (converter == null) + if (!source.HasValue) { - throw new ArgumentNullException(nameof(converter)); + return new OptionElse(); } - if (fallbackConverter == null) + if (!source.Value.HasValue) { - throw new ArgumentNullException(nameof(fallbackConverter)); + return new OptionElse(); } - return source.HasValue ? converter(source.Value) : fallbackConverter(); - } - - /// - /// Converts the specified source. - /// - /// The type of the source. - /// The type of the destination. - /// The source. - /// The converter. - /// - /// converter - public static Optional Convert(this Optional source, Func converter) - { - if (converter == null) + if (action is null) { - throw new ArgumentNullException(nameof(converter)); + throw new ArgumentNullException(nameof(action)); } - return source.HasValue ? converter(source.Value) : Optional.None(); - } - - /// - /// Filters where Optional has a value - /// and return the values only - /// - /// The source. - /// - public static IEnumerable SelectValues(this IEnumerable> source) - { - return source.Where(t => t.HasValue).Select(t => t.Value); + action(source.Value.Value); + return OptionElse.NoAction; } /// - /// Overloads a TryGetValue of the dictionary wrapping the result as an Optional + /// Overloads a TryGetValue of the dictionary wrapping the result as an Optional. /// &gt;TValue /// /// @@ -146,30 +141,29 @@ public static IEnumerable SelectValues(this IEnumerable> sourc /// The type of the key. /// The source. /// The key. - /// + /// The option of the looked up value. public static Optional Lookup(this IDictionary source, TKey key) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - TValue contained; - bool result = source.TryGetValue(key, out contained); + bool result = source.TryGetValue(key, out var contained); return result ? contained : Optional.None(); } /// - /// Removes item if contained in the cache + /// Removes item if contained in the cache. /// /// The type of the value. /// The type of the key. /// The source. /// The key. - /// + /// If the item was removed. public static bool RemoveIfContained(this IDictionary source, TKey key) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -178,46 +172,85 @@ public static bool RemoveIfContained(this IDictionary - /// Overloads Enumerable.FirstOrDefault() and wraps the result in a Optional - /// &gt;T - /// container + /// Filters where Optional has a value + /// and return the values only. /// - /// + /// The type of item. /// The source. - /// The selector. - /// - public static Optional FirstOrOptional(this IEnumerable source, Func selector) + /// An enumerable of the selected items. + public static IEnumerable SelectValues(this IEnumerable> source) + { + return source.Where(t => t.HasValue && t.Value is not null).Select(t => t.Value!); + } + + /// + /// Returns the value if the nullable has a value, otherwise returns the result of the value selector. + /// + /// The type of the item. + /// The source. + /// The default value. + /// The value or the default value. + public static T ValueOr(this T? source, T defaultValue) + where T : struct { - if (source == null) + return source ?? defaultValue; + } + + /// + /// Returns the value if the optional has a value, otherwise returns the result of the value selector. + /// + /// The type of the item. + /// The source. + /// The value selector. + /// If the value or a provided default. + /// valueSelector. + public static T ValueOr(this Optional source, Func valueSelector) + { + if (valueSelector is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(valueSelector)); } - var result = source.FirstOrDefault(selector); - return !Equals(result, null) ? result : Optional.None(); + return source.HasValue ? source.Value : valueSelector(); } /// - /// Invokes the specified action when. + /// Returns the value if the optional has a value, otherwise returns the default value of T. /// - /// + /// The type of the item. /// The source. - /// The action. - /// The optional else extension. - public static OptionElse IfHasValue(this Optional source, Action action) + /// The value or default. + public static T? ValueOrDefault(this Optional source) { - if (!source.HasValue) + if (source.HasValue) { - return new OptionElse(); + return source.Value; + } + + return default; + } + + /// + /// Returns the value if the optional has a value, otherwise throws an exception as specified by the exception generator. + /// + /// The type of the item. + /// The source. + /// The exception generator. + /// The value. + /// exceptionGenerator. + public static T ValueOrThrow(this Optional source, Func exceptionGenerator) + { + if (exceptionGenerator is null) + { + throw new ArgumentNullException(nameof(exceptionGenerator)); } - if (action == null) + if (source.HasValue && source.Value is not null) { - throw new ArgumentNullException(nameof(action)); + return source.Value; } - action(source.Value); - return OptionElse.NoAction; + throw exceptionGenerator(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/Optional.cs b/src/DynamicData/Kernel/Optional.cs index 8237ecc1c..0e2319d2b 100644 --- a/src/DynamicData/Kernel/Optional.cs +++ b/src/DynamicData/Kernel/Optional.cs @@ -1,58 +1,30 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + #pragma warning disable 1591 namespace DynamicData.Kernel { /// - /// Optional factory class - /// - public static class Optional - { - /// - ///Wraps the specified value in an Optional container - /// - /// - /// The value. - /// - public static Optional Some(T value) - { - return new Optional(value); - } - - /// - /// Returns an None optional value for the specified type. - /// - /// - /// - public static Optional None() - { - return Optional.None; - } - } - - /// - /// The equivalent of a nullable type which works on value and reference types + /// The equivalent of a nullable type which works on value and reference types. /// - /// The underlying value type of the generic type.1 + /// The underlying value type of the generic type.1. + [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Deliberate usage.")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Class names the same, generic differences.")] public readonly struct Optional : IEquatable> { - private readonly T _value; - - /// - /// The default valueless optional - /// - public static readonly Optional None; + private readonly T? _value; /// /// Initializes a new instance of the struct. /// /// The value. - internal Optional(T value) + internal Optional(T? value) { if (ReferenceEquals(value, null)) { @@ -67,37 +39,32 @@ internal Optional(T value) } /// - /// Creates the specified value. + /// Gets the default valueless optional. /// - /// The value. - /// - public static Optional Create(T value) - { - return new Optional(value); - } + public static Optional None { get; } /// - /// Gets a value indicating whether the current object has a value. + /// Gets a value indicating whether the current object has a value. /// /// /// - /// true if the current object has a value; false if the current object has no value. + /// true if the current object has a value; false if the current object has no value. /// public bool HasValue { get; } /// - /// Gets the value of the current value. + /// Gets the value of the current value. /// /// /// - /// The value of the current object if the property is true. An exception is thrown if the property is false. + /// The value of the current object if the property is true. An exception is thrown if the property is false. /// - /// The property is false. + /// The property is false. public T Value { get { - if (!HasValue) + if (!HasValue || _value is null) { throw new InvalidOperationException("Optional has no value"); } @@ -107,45 +74,63 @@ public T Value } /// - /// Implicit cast from the vale to the optional + /// Implicit cast from the vale to the optional. /// /// The value. - /// - public static implicit operator Optional(T value) + /// The optional value. + public static implicit operator Optional(T? value) { return ToOptional(value); } /// - /// Explicit cast from option to value + /// Explicit cast from option to value. /// /// The value. - /// - public static explicit operator T(Optional value) + /// The optional value. + public static explicit operator T?(Optional value) { return FromOptional(value); } - public static T FromOptional(Optional value) + public static bool operator ==(Optional left, Optional right) { - return value.Value; + return left.Equals(right); } - public static Optional ToOptional(T value) + public static bool operator !=(Optional left, Optional right) { - return new Optional(value); + return !left.Equals(right); } - #region Equality members + /// + /// Creates the specified value. + /// + /// The value. + /// The optional value. + public static Optional Create(T? value) + { + return new(value); + } - public static bool operator ==(Optional left, Optional right) + /// + /// Gets the value from the optional value. + /// + /// The optional value. + /// The value. + public static T? FromOptional(Optional value) { - return left.Equals(right); + return value.Value; } - public static bool operator !=(Optional left, Optional right) + /// + /// Gets the optional from a value. + /// + /// The value to get the optional for. + /// The optional. + public static Optional ToOptional(T? value) { - return !left.Equals(right); + return new(value); } /// @@ -161,18 +146,28 @@ public bool Equals(Optional other) return false; } + if (_value is null && other._value is null) + { + return true; + } + + if (_value is null || other._value is null) + { + return false; + } + return HasValue.Equals(other.HasValue) && EqualityComparer.Default.Equals(_value, other._value); } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { return false; } - return obj is Optional && Equals((Optional)obj); + return obj is Optional optional && Equals(optional); } /// @@ -180,16 +175,51 @@ public override int GetHashCode() { unchecked { + if (_value is null) + { + return 0; + } + return (HasValue.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(_value); } } - #endregion - /// - public override string ToString() + public override string? ToString() { + if (_value is null) + { + return ""; + } + return !HasValue ? "" : _value.ToString(); } } -} + + /// + /// Optional factory class. + /// + public static class Optional + { + /// + /// Returns an None optional value for the specified type. + /// + /// The type of the item. + /// The optional value. + public static Optional None() + { + return Optional.None; + } + + /// + /// Wraps the specified value in an Optional container. + /// + /// The type of the item. + /// The value. + /// The optional value. + public static Optional Some(T value) + { + return new(value); + } + } +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/ParallelEx.cs b/src/DynamicData/Kernel/ParallelEx.cs index fd26e52ba..9773a5078 100644 --- a/src/DynamicData/Kernel/ParallelEx.cs +++ b/src/DynamicData/Kernel/ParallelEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -15,12 +15,12 @@ internal static class ParallelEx [SuppressMessage("Design", "CA2000: Dispose SemaphoreSlim", Justification = "Captured in lambda, can cause problems.")] public static async Task> SelectParallel(this IEnumerable source, Func> selector, int maximumThreads = 5) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (selector == null) + if (selector is null) { throw new ArgumentNullException(nameof(selector)); } @@ -32,21 +32,22 @@ public static async Task> SelectParallel - { - try - { - return await selector(item).ConfigureAwait(false); - } - finally - { - semaphore.Release(); - } - })); + tasks.Add( + Task.Run( + async () => + { + try + { + return await selector(item).ConfigureAwait(false); + } + finally + { + semaphore.Release(); + } + })); } return await Task.WhenAll(tasks).ConfigureAwait(false); } - } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/ReadOnlyCollectionLight.cs b/src/DynamicData/Kernel/ReadOnlyCollectionLight.cs index 60224fa8b..034a11e13 100644 --- a/src/DynamicData/Kernel/ReadOnlyCollectionLight.cs +++ b/src/DynamicData/Kernel/ReadOnlyCollectionLight.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -12,8 +12,6 @@ internal sealed class ReadOnlyCollectionLight : IReadOnlyCollection { private readonly IList _items; - public static readonly IReadOnlyCollection Empty = new ReadOnlyCollectionLight(); - public ReadOnlyCollectionLight(IEnumerable items) { _items = items.ToList(); @@ -25,6 +23,10 @@ private ReadOnlyCollectionLight() _items = new List(); } + public static IReadOnlyCollection Empty { get; } = new ReadOnlyCollectionLight(); + + public int Count { get; } + public IEnumerator GetEnumerator() { return _items.GetEnumerator(); @@ -34,7 +36,5 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } - - public int Count { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/Kernel/ReferenceEqualityComparer.cs b/src/DynamicData/Kernel/ReferenceEqualityComparer.cs index 12f2d576f..7b1885470 100644 --- a/src/DynamicData/Kernel/ReferenceEqualityComparer.cs +++ b/src/DynamicData/Kernel/ReferenceEqualityComparer.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,14 +10,14 @@ internal class ReferenceEqualityComparer : IEqualityComparer { public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); - public bool Equals(T x, T y) + public bool Equals(T? x, T? y) { return ReferenceEquals(x, y); } - public int GetHashCode(T obj) + public int GetHashCode(T? obj) { - return obj.GetHashCode(); + return obj is null ? 0 : obj.GetHashCode(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Change.cs b/src/DynamicData/List/Change.cs index da78088f8..965590d3e 100644 --- a/src/DynamicData/List/Change.cs +++ b/src/DynamicData/List/Change.cs @@ -1,45 +1,23 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; + #pragma warning disable 1591 // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Container to describe a single change to a cache + /// Container to describe a single change to a cache. /// + /// The type of the item. public sealed class Change : IEquatable> { - /// - /// The reason for the change - /// - public ListChangeReason Reason { get; } - - /// - /// A single item change - /// - public ItemChange Item { get; } - - /// - /// A multiple item change - /// - public RangeChange Range { get; } - - /// - /// Gets a value indicating whether the change is a single item change or a range change - /// - /// - /// The type. - /// - public ChangeType Type => Reason.GetChangeType(); - - #region Construction - /// /// Initializes a new instance of the class. /// @@ -64,9 +42,9 @@ public Change(ListChangeReason reason, IEnumerable items, int index = -1) throw new IndexOutOfRangeException("ListChangeReason must be a range type for a range change"); } - //ignore this case because WhereReasonsAre removes the index - //if (reason== ListChangeReason.RemoveRange && index < 0) - // throw new UnspecifiedIndexException("ListChangeReason.RemoveRange should not have an index specified index"); + //// ignore this case because WhereReasonsAre removes the index + //// if (reason== ListChangeReason.RemoveRange && index < 0) + //// throw new UnspecifiedIndexException("ListChangeReason.RemoveRange should not have an index specified index"); Reason = reason; Item = ItemChange.Empty; @@ -74,7 +52,8 @@ public Change(ListChangeReason reason, IEnumerable items, int index = -1) } /// - /// Constructor for ChangeReason.Move + /// Initializes a new instance of the class. + /// Constructor for ChangeReason.Move. /// /// The current. /// The CurrentIndex. @@ -82,7 +61,7 @@ public Change(ListChangeReason reason, IEnumerable items, int index = -1) /// /// CurrentIndex must be greater than or equal to zero /// or - /// PreviousIndex must be greater than or equal to zero + /// PreviousIndex must be greater than or equal to zero. /// public Change(T current, int currentIndex, int previousIndex) { @@ -98,9 +77,11 @@ public Change(T current, int currentIndex, int previousIndex) Reason = ListChangeReason.Moved; Item = new ItemChange(Reason, current, Optional.None(), currentIndex, previousIndex); + Range = RangeChange.Empty; } /// + /// Initializes a new instance of the class. /// Initializes a new instance of the struct. /// /// The reason. @@ -111,11 +92,8 @@ public Change(T current, int currentIndex, int previousIndex) /// /// For ChangeReason.Add, a previous value cannot be specified /// or - /// For ChangeReason.Change, must supply previous value + /// For ChangeReason.Change, must supply previous value. /// - /// For ChangeReason.Add, a previous value cannot be specified - /// or - /// For ChangeReason.Change, must supply previous value public Change(ListChangeReason reason, T current, Optional previous, int currentIndex = -1, int previousIndex = -1) { if (reason == ListChangeReason.Add && previous.HasValue) @@ -135,14 +113,38 @@ public Change(ListChangeReason reason, T current, Optional previous, int curr Reason = reason; Item = new ItemChange(Reason, current, previous, currentIndex, previousIndex); + Range = RangeChange.Empty; } - #endregion + /// + /// Gets a single item change. + /// + public ItemChange Item { get; } - #region Equality + /// + /// Gets a multiple item change. + /// + public RangeChange Range { get; } + + /// + /// Gets the reason for the change. + /// + public ListChangeReason Reason { get; } + + /// + /// Gets a value indicating whether the change is a single item change or a range change. + /// + /// + /// The type. + /// + public ChangeType Type => Reason.GetChangeType(); + + public static bool operator ==(Change left, Change right) => Equals(left, right); + + public static bool operator !=(Change left, Change right) => !Equals(left, right); /// - public bool Equals(Change other) + public bool Equals(Change? other) { if (ReferenceEquals(null, other)) { @@ -158,7 +160,7 @@ public bool Equals(Change other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -185,22 +187,15 @@ public override int GetHashCode() { var hashCode = (int)Reason; hashCode = (hashCode * 397) ^ Item.GetHashCode(); - hashCode = (hashCode * 397) ^ (Range?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ Range.GetHashCode(); return hashCode; } } - public static bool operator ==(Change left, Change right) => Equals(left, right); - - public static bool operator !=(Change left, Change right) => !Equals(left, right); - - #endregion - /// public override string ToString() { - return Range != null ? $"{Reason}. {Range.Count} changes" - : $"{Reason}. Current: {Item.Current}, Previous: {Item.Previous}"; + return $"{Reason}. {Range.Count} changes"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ChangeAwareList.cs b/src/DynamicData/List/ChangeAwareList.cs index 673d85faa..f019cea4b 100644 --- a/src/DynamicData/List/ChangeAwareList.cs +++ b/src/DynamicData/List/ChangeAwareList.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,9 +6,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; + using DynamicData.Kernel; -// ReSharper disable once CheckNamespace namespace DynamicData { /// @@ -16,27 +16,34 @@ namespace DynamicData /// /// Used for creating custom operators. /// + /// The item type. /// public class ChangeAwareList : IExtendedList { - private readonly object _lockObject = new object(); private readonly List _innerList; - private ChangeSet _changes = new ChangeSet(); + + private readonly object _lockObject = new(); + + private ChangeSet _changes = new(); /// - /// Create a change aware list with the specified capacity + /// Initializes a new instance of the class. + /// Create a change aware list with the specified capacity. /// + /// The initial capacity of the internal lists. public ChangeAwareList(int capacity = -1) { _innerList = capacity > 0 ? new List(capacity) : new List(); } /// - /// Create a change aware list with the specified items + /// Initializes a new instance of the class. + /// Create a change aware list with the specified items. /// + /// The items to seed the change aware list with. public ChangeAwareList(IEnumerable items) { - if (items == null) + if (items is null) { throw new ArgumentNullException(nameof(items)); } @@ -45,20 +52,21 @@ public ChangeAwareList(IEnumerable items) _innerList = new List(list); - if (_innerList.Any()) + if (_innerList.Count > 0) { _changes.Add(new Change(ListChangeReason.AddRange, list)); } } /// - /// Clone an existing ChangeAwareList + /// Initializes a new instance of the class. + /// Clone an existing ChangeAwareList. /// - /// The original ChangeAwareList to copy - /// Should the list of changes also be copied over? + /// The original ChangeAwareList to copy. + /// Should the list of changes also be copied over?. public ChangeAwareList(ChangeAwareList list, bool copyChanges) { - if (list == null) + if (list is null) { throw new ArgumentNullException(nameof(list)); } @@ -72,51 +80,71 @@ public ChangeAwareList(ChangeAwareList list, bool copyChanges) } /// - /// Create a changeset from recorded changes and clears known changes. + /// Gets or sets the total number of elements the internal data structure can hold without resizing. /// - public IChangeSet CaptureChanges() + public int Capacity { - ChangeSet returnValue; - lock (_lockObject) + get => _innerList.Capacity; + set { - if (_changes.Count == 0) + lock (_lockObject) { - return ChangeSet.Empty; + _innerList.Capacity = value; } + } + } - returnValue = _changes; + /// + /// Gets the element count. + /// + public int Count => _innerList.Count; - //we can infer this is a Clear - if (_innerList.Count == 0 && returnValue.Removes == returnValue.TotalChanges && returnValue.TotalChanges > 1) + /// + /// Gets a value indicating whether is this collection read only. + /// + public bool IsReadOnly => false; + + /// + /// Gets the last change in the collection. + /// + private Optional> Last + { + get + { + lock (_lockObject) { - var removed = returnValue.Unified().Select(u => u.Current); - returnValue = new ChangeSet { new Change(ListChangeReason.Clear, removed) }; + return _changes.Count == 0 ? Optional.None>() : _changes[_changes.Count - 1]; } - - ClearChanges(); } + } - return returnValue; + /// + /// Gets or sets the item at the specified index. + /// + /// The index to set. + public T this[int index] + { + get => _innerList[index]; + set => SetItem(index, value); } /// - /// Clears the changes (for testing). + /// Adds the item to the end of the collection. /// - internal void ClearChanges() + /// The item to add. + public void Add(T item) { lock (_lockObject) { - _changes = new ChangeSet(); + InsertItem(_innerList.Count, item); } } - #region Range support - /// /// Adds the elements of the specified collection to the end of the collection. /// /// The items to add. - /// is null. + /// is null. public void AddRange(IEnumerable collection) { var args = new Change(ListChangeReason.AddRange, collection); @@ -134,61 +162,36 @@ public void AddRange(IEnumerable collection) } /// - /// Inserts the elements of a collection into the at the specified index. - /// - /// Inserts the specified items - /// The zero-based index at which the new elements should be inserted. - /// is null. - /// is less than 0.-or- is greater than . - public void InsertRange(IEnumerable collection, int index) - { - var args = new Change(ListChangeReason.AddRange, collection, index); - if (args.Range.Count == 0) - { - return; - } - - lock (_lockObject) - { - _changes.Add(args); - _innerList.InsertRange(index, args.Range); - } - - OnInsertItems(index, args.Range); - } - - /// - /// Removes a range of elements from the . + /// Create a change set from recorded changes and clears known changes. /// - /// The zero-based starting index of the range of elements to remove.The number of elements to remove. is less than 0.-or- is less than 0. and do not denote a valid range of elements in the . - - public void RemoveRange(int index, int count) + /// The change set. + public IChangeSet CaptureChanges() { - Change args; + ChangeSet returnValue; lock (_lockObject) { - if (index >= _innerList.Count || index + count > _innerList.Count) + if (_changes.Count == 0) { - throw new ArgumentOutOfRangeException(nameof(index)); + return ChangeSet.Empty; } - var toremove = _innerList.Skip(index).Take(count).ToList(); - if (toremove.Count == 0) + returnValue = _changes; + + // we can infer this is a Clear + if (_innerList.Count == 0 && returnValue.Removes == returnValue.TotalChanges && returnValue.TotalChanges > 1) { - return; + var removed = returnValue.Unified().Select(u => u.Current); + returnValue = new ChangeSet { new(ListChangeReason.Clear, removed) }; } - args = new Change(ListChangeReason.RemoveRange, toremove, index); - - _changes.Add(args); - _innerList.RemoveRange(index, count); + ClearChanges(); } - OnRemoveItems(index, args.Range); + return returnValue; } /// - /// Removes all elements from the list + /// Removes all elements from the list. /// public virtual void Clear() { @@ -199,48 +202,81 @@ public virtual void Clear() return; } - var toremove = _innerList.ToList(); + var toRemove = _innerList.ToList(); - _changes.Add(new Change(ListChangeReason.Clear, toremove)); + _changes.Add(new Change(ListChangeReason.Clear, toRemove)); _innerList.Clear(); } } - #endregion - - #region Subclass overrides - /// - /// Override for custom Set + /// Determines whether the element is in the collection. /// - protected virtual void OnSetItem(int index, T newItem, T oldItem) + /// The item to check. + /// If the item is contained or not. + public virtual bool Contains(T item) { + lock (_lockObject) + { + return _innerList.Contains(item); + } } /// - /// Override for custom Insert + /// Copies the entire collection to a compatible one-dimensional array, starting at the specified index of the target array. /// - protected virtual void OnInsertItems(int startIndex, IEnumerable items) + /// The array to copy to. + /// The index to start copying to. + public void CopyTo(T[] array, int arrayIndex) + { + lock (_lockObject) + { + _innerList.CopyTo(array, arrayIndex); + } + } + + /// + public IEnumerator GetEnumerator() { + lock (_lockObject) + { + return _innerList.ToList().GetEnumerator(); + } } /// - /// Override for custom remove + /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire collection. /// - protected virtual void OnRemoveItems(int startIndex, IEnumerable items) + /// The item to get the index of. + /// The index. + public int IndexOf(T item) { + lock (_lockObject) + { + return _innerList.IndexOf(item); + } } - #endregion - - #region Refresh + /// + /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire collection, using the specified comparer. + /// + /// The item to get the index of. + /// The equality comparer to use to compare. + /// The index. + public int IndexOf(T item, IEqualityComparer equalityComparer) + { + lock (_lockObject) + { + return _innerList.IndexOf(item, equalityComparer); + } + } /// - /// Add a Refresh change of the item at the specified index to the list of changes. - /// - /// This is to notify downstream operators to refresh. + /// Inserts an element into the list at the specified index. /// - public void RefreshAt(int index) + /// The index to insert at. + /// The item to insert. + public void Insert(int index, T item) { if (index < 0) { @@ -254,7 +290,91 @@ public void RefreshAt(int index) throw new ArgumentException($"{nameof(index)} cannot be greater than the size of the collection"); } - _changes.Add(new Change(ListChangeReason.Refresh, _innerList[index], index)); + InsertItem(index, item); + } + } + + /// + /// Inserts the elements of a collection into the at the specified index. + /// + /// Inserts the specified items. + /// The zero-based index at which the new elements should be inserted. + /// is null. + /// is less than 0.-or- is greater than . + public void InsertRange(IEnumerable collection, int index) + { + var args = new Change(ListChangeReason.AddRange, collection, index); + if (args.Range.Count == 0) + { + return; + } + + lock (_lockObject) + { + _changes.Add(args); + _innerList.InsertRange(index, args.Range); + } + + OnInsertItems(index, args.Range); + } + + /// + /// Moves the item to the specified destination index. + /// + /// The item to move. + /// The destination index. + public virtual void Move(T item, int destination) + { + if (destination < 0) + { + throw new ArgumentException($"{nameof(destination)} cannot be negative"); + } + + lock (_lockObject) + { + if (destination > _innerList.Count) + { + throw new ArgumentException($"{nameof(destination)} cannot be greater than the size of the collection"); + } + + int index = _innerList.IndexOf(item); + Move(index, destination); + } + } + + /// + /// Moves an item from the original to the destination index. + /// + /// The original. + /// The destination. + public virtual void Move(int original, int destination) + { + if (original < 0) + { + throw new ArgumentException($"{nameof(original)} cannot be negative"); + } + + if (destination < 0) + { + throw new ArgumentException($"{nameof(destination)} cannot be negative"); + } + + lock (_lockObject) + { + if (original > _innerList.Count) + { + throw new ArgumentException($"{nameof(original)} cannot be greater than the size of the collection"); + } + + if (destination > _innerList.Count) + { + throw new ArgumentException($"{nameof(destination)} cannot be greater than the size of the collection"); + } + + var item = _innerList[original]; + _innerList.RemoveAt(original); + _innerList.Insert(destination, item); + _changes.Add(new Change(item, destination, original)); } } @@ -263,7 +383,8 @@ public void RefreshAt(int index) /// /// This is to notify downstream operators to refresh. /// - /// If the item is in the list, returns true + /// The item to refresh. + /// The index to refresh. public void Refresh(T item, int index) { if (index < 0) @@ -289,7 +410,8 @@ public void Refresh(T item, int index) /// Add a Refresh change for specified index to the list of changes. /// This is to notify downstream operators to refresh. /// - /// If the item is in the list, returns true + /// The item to refresh. + /// If the item is in the list, returns true. public bool Refresh(T item) { var index = IndexOf(item); @@ -306,50 +428,143 @@ public bool Refresh(T item) return true; } - #endregion + /// + /// Add a Refresh change of the item at the specified index to the list of changes. + /// + /// This is to notify downstream operators to refresh. + /// + /// The index to refresh. + public void RefreshAt(int index) + { + if (index < 0) + { + throw new ArgumentException($"{nameof(index)} cannot be negative"); + } + + lock (_lockObject) + { + if (index > _innerList.Count) + { + throw new ArgumentException($"{nameof(index)} cannot be greater than the size of the collection"); + } - #region Collection overrides + _changes.Add(new Change(ListChangeReason.Refresh, _innerList[index], index)); + } + } /// - /// Gets the last change in the collection + /// Removes the item from the collection and returns true if the item was successfully removed. /// - private Optional> Last + /// The item to remove. + /// If the item was removed. + public bool Remove(T item) { - get + lock (_lockObject) { - lock (_lockObject) + var index = _innerList.IndexOf(item); + if (index < 0) { - return _changes.Count == 0 ? Optional.None>() : _changes[_changes.Count - 1]; + return false; } + + RemoveItem(index, item); + return true; } } /// - /// Inserts an item at the specified index + /// Removes the item from the specified index. /// - /// the index where the item should be inserted - /// - protected virtual void InsertItem(int index, T item) + /// The index to remove the item at. + public void RemoveAt(int index) { if (index < 0) { throw new ArgumentException($"{nameof(index)} cannot be negative"); } - if (index > _innerList.Count) - { - throw new ArgumentException($"{nameof(index)} cannot be greater than the size of the collection"); - } - lock (_lockObject) { - //attempt to batch updates as lists love to deal with ranges! (sorry if this code melts your mind) + if (index > _innerList.Count) + { + throw new ArgumentOutOfRangeException($"{nameof(index)} cannot be greater than the size of the collection"); + } + + RemoveItem(index); + } + } + + /// + /// Removes a range of elements from the . + /// + /// The zero-based starting index of the range of elements to remove.The number of elements to remove. is less than 0.-or- is less than 0. and do not denote a valid range of elements in the . + public void RemoveRange(int index, int count) + { + Change args; + lock (_lockObject) + { + if (index >= _innerList.Count || index + count > _innerList.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + var toRemove = _innerList.Skip(index).Take(count).ToList(); + if (toRemove.Count == 0) + { + return; + } + + args = new Change(ListChangeReason.RemoveRange, toRemove, index); + + _changes.Add(args); + _innerList.RemoveRange(index, count); + } + + OnRemoveItems(index, args.Range); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Clears the changes (for testing). + /// + internal void ClearChanges() + { + lock (_lockObject) + { + _changes = new ChangeSet(); + } + } + + /// + /// Inserts an item at the specified index. + /// + /// the index where the item should be inserted. + /// The item to insert. + protected virtual void InsertItem(int index, T item) + { + if (index < 0) + { + throw new ArgumentException($"{nameof(index)} cannot be negative"); + } + + if (index > _innerList.Count) + { + throw new ArgumentException($"{nameof(index)} cannot be greater than the size of the collection"); + } + + lock (_lockObject) + { + // attempt to batch updates as lists love to deal with ranges! (sorry if this code melts your mind) var last = Last; if (last.HasValue && last.Value.Reason == ListChangeReason.Add) { - //begin a new batch if possible - + // begin a new batch if possible var firstOfBatch = _changes.Count - 1; var previousItem = last.Value.Item; @@ -368,7 +583,7 @@ protected virtual void InsertItem(int index, T item) } else if (last.HasValue && last.Value.Reason == ListChangeReason.AddRange) { - //check whether the new item is in the specified range + // check whether the new item is in the specified range var range = last.Value.Range; var minimum = Math.Max(range.Index - 1, 0); @@ -401,18 +616,47 @@ protected virtual void InsertItem(int index, T item) } else { - //first add, so cannot infer range + // first add, so cannot infer range _changes.Add(new Change(ListChangeReason.Add, item, index)); } - //finally, add the item + // finally, add the item _innerList.Insert(index, item); } } /// - /// Remove the item which is at the specified index + /// Override for custom Insert. + /// + /// The starting index of the items being inserted. + /// The items being inserted. + protected virtual void OnInsertItems(int startIndex, IEnumerable items) + { + } + + /// + /// Override for custom remove. + /// + /// The starting index of the items being removed. + /// The items being removed. + protected virtual void OnRemoveItems(int startIndex, IEnumerable items) + { + } + + /// + /// Override for custom Set. + /// + /// The index of the item set. + /// The new item. + /// The old item. + protected virtual void OnSetItem(int index, T newItem, T oldItem) + { + } + + /// + /// Remove the item which is at the specified index. /// + /// The index being removed. protected void RemoveItem(int index) { lock (_lockObject) @@ -423,8 +667,10 @@ protected void RemoveItem(int index) } /// - /// Removes the item from the specified index - intended for internal use only + /// Removes the item from the specified index - intended for internal use only. /// + /// The index being removed. + /// The item being removed. protected virtual void RemoveItem(int index, T item) { if (index < 0) @@ -439,11 +685,11 @@ protected virtual void RemoveItem(int index, T item) throw new ArgumentException($"{nameof(index)} cannot be greater than the size of the collection"); } - //attempt to batch updates as lists love to deal with ranges! (sorry if this code melts your mind) + // attempt to batch updates as lists love to deal with ranges! (sorry if this code melts your mind) var last = Last; if (last.HasValue && last.Value.Reason == ListChangeReason.Remove) { - //begin a new batch + // begin a new batch var firstOfBatch = _changes.Count - 1; var previousItem = last.Value.Item; @@ -451,10 +697,9 @@ protected virtual void RemoveItem(int index, T item) { _changes[firstOfBatch] = new Change(ListChangeReason.RemoveRange, new[] { previousItem.Current, item }, index); } - else if (index == previousItem.CurrentIndex - 1) { - //Nb: double check this one as it is the same as clause above. Can it be correct? + // Nb: double check this one as it is the same as clause above. Can it be correct? _changes[firstOfBatch] = new Change(ListChangeReason.RemoveRange, new[] { item, previousItem.Current }, index); } else @@ -464,11 +709,11 @@ protected virtual void RemoveItem(int index, T item) } else if (last.HasValue && last.Value.Reason == ListChangeReason.RemoveRange) { - //add to the end of the previous batch + // add to the end of the previous batch var range = last.Value.Range; if (range.Index == index) { - //removed in order + // removed in order range.Add(item); } else if (range.Index == index - 1) @@ -477,7 +722,7 @@ protected virtual void RemoveItem(int index, T item) } else if (range.Index == index + 1) { - //removed in reverse order + // removed in reverse order range.Insert(0, item); range.SetStartingIndex(index); } @@ -488,7 +733,7 @@ protected virtual void RemoveItem(int index, T item) } else { - //first remove, so cannot infer range + // first remove, so cannot infer range _changes.Add(new Change(ListChangeReason.Remove, item, index)); } @@ -499,6 +744,8 @@ protected virtual void RemoveItem(int index, T item) /// /// Replaces the element which is as the specified index wth the specified item. /// + /// The index of the item to set. + /// The item to set. protected virtual void SetItem(int index, T item) { if (index < 0) @@ -522,240 +769,5 @@ protected virtual void SetItem(int index, T item) OnSetItem(index, item, previous); } - - /// - /// Moves the item to the specified destination index - /// - /// - /// - public virtual void Move(T item, int destination) - { - if (destination < 0) - { - throw new ArgumentException($"{nameof(destination)} cannot be negative"); - } - - lock (_lockObject) - { - if (destination > _innerList.Count) - { - throw new ArgumentException($"{nameof(destination)} cannot be greater than the size of the collection"); - } - - int index = _innerList.IndexOf(item); - Move(index, destination); - } - } - - /// - /// Moves an item from the original to the destination index - /// - /// The original. - /// The destination. - public virtual void Move(int original, int destination) - { - if (original < 0) - { - throw new ArgumentException($"{nameof(original)} cannot be negative"); - } - - if (destination < 0) - { - throw new ArgumentException($"{nameof(destination)} cannot be negative"); - } - - lock (_lockObject) - { - if (original > _innerList.Count) - { - throw new ArgumentException($"{nameof(original)} cannot be greater than the size of the collection"); - } - - if (destination > _innerList.Count) - { - throw new ArgumentException($"{nameof(destination)} cannot be greater than the size of the collection"); - } - - var item = _innerList[original]; - _innerList.RemoveAt(original); - _innerList.Insert(destination, item); - _changes.Add(new Change(item, destination, original)); - } - } - - #endregion - - #region ISupportsCapcity - - /// - /// Gets or sets the total number of elements the internal data structure can hold without resizing. - /// - public int Capacity - { - get => _innerList.Capacity; - set - { - lock (_lockObject) - { - _innerList.Capacity = value; - } - } - } - - /// - /// Gets the element count - /// - public int Count => _innerList.Count; - - #endregion - - #region IList implementation - - /// - /// Determines whether the element is in the collection. - /// - public virtual bool Contains(T item) - { - lock (_lockObject) - { - return _innerList.Contains(item); - } - } - - /// - /// Copies the entire collection to a compatible one-dimensional array, starting at the specified index of the target array. - /// - public void CopyTo(T[] array, int arrayIndex) - { - lock (_lockObject) - { - _innerList.CopyTo(array, arrayIndex); - } - } - - /// - /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire collection. - /// - public int IndexOf(T item) - { - lock (_lockObject) - { - return _innerList.IndexOf(item); - } - } - - /// - /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire collection, using the specified comparer - /// - public int IndexOf(T item, IEqualityComparer equalityComparer) - { - lock (_lockObject) - { - return _innerList.IndexOf(item, equalityComparer); - } - } - - /// - /// Inserts an element into the list at the specified index. - /// - public void Insert(int index, T item) - { - if (index < 0) - { - throw new ArgumentException($"{nameof(index)} cannot be negative"); - } - - lock (_lockObject) - { - if (index > _innerList.Count) - { - throw new ArgumentException($"{nameof(index)} cannot be greater than the size of the collection"); - } - - InsertItem(index, item); - } - } - - /// - /// Removes the item from the specified index - /// - /// - public void RemoveAt(int index) - { - if (index < 0) - { - throw new ArgumentException($"{nameof(index)} cannot be negative"); - } - - lock (_lockObject) - { - if (index > _innerList.Count) - { - throw new ArgumentOutOfRangeException($"{nameof(index)} cannot be greater than the size of the collection"); - } - - RemoveItem(index); - } - } - - /// - /// Adds the item to the end of the collection - /// - public void Add(T item) - { - lock (_lockObject) - { - InsertItem(_innerList.Count, item); - } - } - - /// - /// Removes the item from the collection and returns true if the item was successfully removed - /// - public bool Remove(T item) - { - lock (_lockObject) - { - var index = _innerList.IndexOf(item); - if (index < 0) - { - return false; - } - - RemoveItem(index, item); - return true; - } - } - - /// - /// Gets or sets the item at the specified index - /// - public T this[int index] - { - get => _innerList[index]; - set => SetItem(index, value); - } - - /// - public IEnumerator GetEnumerator() - { - lock (_lockObject) - { - return _innerList.ToList().GetEnumerator(); - } - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// Is this collection read only - /// - public bool IsReadOnly => false; - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ChangeAwareListWithRefCounts.cs b/src/DynamicData/List/ChangeAwareListWithRefCounts.cs index fd17744d7..3e973c2e5 100644 --- a/src/DynamicData/List/ChangeAwareListWithRefCounts.cs +++ b/src/DynamicData/List/ChangeAwareListWithRefCounts.cs @@ -1,8 +1,9 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections.Generic; + using DynamicData.Kernel; using DynamicData.List.Internal; @@ -11,7 +12,18 @@ namespace DynamicData { internal class ChangeAwareListWithRefCounts : ChangeAwareList { - private readonly ReferenceCountTracker _tracker = new ReferenceCountTracker(); + private readonly ReferenceCountTracker _tracker = new(); + + public override void Clear() + { + _tracker.Clear(); + base.Clear(); + } + + public override bool Contains(T item) + { + return _tracker.Contains(item); + } protected override void InsertItem(int index, T item) { @@ -24,12 +36,6 @@ protected override void OnInsertItems(int startIndex, IEnumerable items) items.ForEach(t => _tracker.Add(t)); } - protected override void RemoveItem(int index, T item) - { - _tracker.Remove(item); - base.RemoveItem(index, item); - } - protected override void OnRemoveItems(int startIndex, IEnumerable items) { items.ForEach(t => _tracker.Remove(t)); @@ -42,15 +48,10 @@ protected override void OnSetItem(int index, T newItem, T oldItem) base.OnSetItem(index, newItem, oldItem); } - public override bool Contains(T item) - { - return _tracker.Contains(item); - } - - public override void Clear() + protected override void RemoveItem(int index, T item) { - _tracker.Clear(); - base.Clear(); + _tracker.Remove(item); + base.RemoveItem(index, item); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ChangeSet.cs b/src/DynamicData/List/ChangeSet.cs index 0f43aa3c9..7411cee94 100644 --- a/src/DynamicData/List/ChangeSet.cs +++ b/src/DynamicData/List/ChangeSet.cs @@ -1,24 +1,21 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. -using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using DynamicData.Annotations; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// A set of changes which has occured since the last reported change + /// A set of changes which has occurred since the last reported change. /// /// The type of the object. public class ChangeSet : List>, IChangeSet { /// - /// An empty change set + /// An empty change set. /// public static readonly IChangeSet Empty = new ChangeSet(); @@ -33,14 +30,14 @@ public ChangeSet() /// Initializes a new instance of the class. /// /// The items. - /// items - public ChangeSet([NotNull] IEnumerable> items) + /// items. + public ChangeSet(IEnumerable> items) : base(items) { } /// - /// Gets the number of additions + /// Gets the number of additions. /// public int Adds { @@ -54,22 +51,29 @@ public int Adds case ListChangeReason.Add: adds++; break; + case ListChangeReason.AddRange: adds += item.Range.Count; break; } } + return adds; } } /// - /// Gets the number of updates + /// Gets the number of moves. /// - public int Replaced => this.Count(c => c.Reason == ListChangeReason.Replace); + public int Moves => this.Count(c => c.Reason == ListChangeReason.Moved); + + /// + /// Gets the number of removes. + /// + public int Refreshes => this.Count(c => c.Reason == ListChangeReason.Refresh); /// - /// Gets the number of removes + /// Gets the number of removes. /// public int Removes { @@ -83,40 +87,37 @@ public int Removes case ListChangeReason.Remove: removes++; break; + case ListChangeReason.RemoveRange: case ListChangeReason.Clear: removes += item.Range.Count; break; } } + return removes; } } /// - /// Gets the number of removes - /// - public int Refreshes => this.Count(c => c.Reason == ListChangeReason.Refresh); - - /// - /// Gets the number of moves + /// Gets the number of updates. /// - public int Moves => this.Count(c => c.Reason == ListChangeReason.Moved); + public int Replaced => this.Count(c => c.Reason == ListChangeReason.Replace); /// - /// The total number if individual item changes + /// Gets the total number if individual item changes. /// public int TotalChanges => Adds + Removes + Replaced + Moves; /// - /// Returns a that represents this instance. + /// Returns a that represents this instance. /// /// - /// A that represents this instance. + /// A that represents this instance. /// public override string ToString() { return $"ChangeSet<{typeof(T).Name}>. Count={Count}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ChangeSetEx.cs b/src/DynamicData/List/ChangeSetEx.cs index 07b8db871..5df7ae465 100644 --- a/src/DynamicData/List/ChangeSetEx.cs +++ b/src/DynamicData/List/ChangeSetEx.cs @@ -1,11 +1,11 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; using DynamicData.List.Internal; using DynamicData.List.Linq; @@ -14,48 +14,20 @@ namespace DynamicData { /// - /// Change set extensions + /// Change set extensions. /// public static class ChangeSetEx { /// - /// Remove the index from the changes + /// Returns a flattened source with the index. /// - /// + /// The type of the item. /// The source. - /// - public static IEnumerable> YieldWithoutIndex(this IEnumerable> source) + /// An enumerable of change sets. + /// source. + public static IEnumerable> Flatten(this IChangeSet source) { - return new WithoutIndexEnumerator(source); - } - - /// - /// Returns a flattend source - /// - /// - /// The source. - /// - /// source - internal static IEnumerable> Unified([NotNull] this IChangeSet source) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return new UnifiedChangeEnumerator(source); - } - - /// - /// Returns a flattend source with the index - /// - /// - /// The source. - /// - /// source - public static IEnumerable> Flatten([NotNull] this IChangeSet source) - { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -64,11 +36,10 @@ public static IEnumerable> Flatten([NotNull] this IChangeSet } /// - /// Gets the type of the change i.e. whether it is an item or a range change + /// Gets the type of the change i.e. whether it is an item or a range change. /// /// The source. - /// - /// + /// The change type. public static ChangeType GetChangeType(this ListChangeReason source) { switch (source) @@ -84,52 +55,77 @@ public static ChangeType GetChangeType(this ListChangeReason source) case ListChangeReason.RemoveRange: case ListChangeReason.Clear: return ChangeType.Range; + default: throw new ArgumentOutOfRangeException(nameof(source)); } } /// - /// Transforms the changeset into a different type using the specified transform function. + /// Transforms the change set into a different type using the specified transform function. /// /// The type of the source. /// The type of the destination. /// The source. /// The transformer. - /// + /// The change set. /// /// source /// or - /// transformer + /// transformer. /// - public static IChangeSet Transform([NotNull] this IChangeSet source, - [NotNull] Func transformer) + public static IChangeSet Transform(this IChangeSet source, Func transformer) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformer == null) + if (transformer is null) { throw new ArgumentNullException(nameof(transformer)); } - var changes = source.Select(change => - { - if (change.Type == ChangeType.Item) - { - return new Change(change.Reason, - transformer(change.Item.Current), - change.Item.Previous.Convert(transformer), - change.Item.CurrentIndex, - change.Item.PreviousIndex); - } + var changes = source.Select( + change => + { + if (change.Type == ChangeType.Item) + { + return new Change(change.Reason, transformer(change.Item.Current), change.Item.Previous.Convert(transformer), change.Item.CurrentIndex, change.Item.PreviousIndex); + } - return new Change(change.Reason, change.Range.Select(transformer), change.Range.Index); - }); + return new Change(change.Reason, change.Range.Select(transformer), change.Range.Index); + }); return new ChangeSet(changes); } + + /// + /// Remove the index from the changes. + /// + /// The type of the item. + /// The source. + /// An enumerable of changes. + public static IEnumerable> YieldWithoutIndex(this IEnumerable> source) + { + return new WithoutIndexEnumerator(source); + } + + /// + /// Returns a flattened source. + /// + /// The type of the item. + /// The source. + /// An enumerable of changes. + /// source. + internal static IEnumerable> Unified(this IChangeSet source) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return new UnifiedChangeEnumerator(source); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ChangeType.cs b/src/DynamicData/List/ChangeType.cs index 1aacf7f56..2a06800af 100644 --- a/src/DynamicData/List/ChangeType.cs +++ b/src/DynamicData/List/ChangeType.cs @@ -1,20 +1,22 @@ +// Copyright (c) 2011-2020 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. -// ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Description of the type of change + /// Description of the type of change. /// public enum ChangeType { /// - /// A single item change + /// A single item change. /// Item, /// - /// A multiple item change + /// A multiple item change. /// Range } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/IChangeSet.cs b/src/DynamicData/List/IChangeSet.cs index e3821174d..96be8f0d8 100644 --- a/src/DynamicData/List/IChangeSet.cs +++ b/src/DynamicData/List/IChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -16,13 +16,13 @@ namespace DynamicData public interface IChangeSet : IEnumerable>, IChangeSet { /// - /// Gets the number of updates + /// Gets the number of updates. /// int Replaced { get; } /// - /// The total count of items changed + /// Gets the total count of items changed. /// int TotalChanges { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/IChangeSetAdaptor.cs b/src/DynamicData/List/IChangeSetAdaptor.cs index 0fd1ca732..ecadcbad4 100644 --- a/src/DynamicData/List/IChangeSetAdaptor.cs +++ b/src/DynamicData/List/IChangeSetAdaptor.cs @@ -1,9 +1,11 @@ +// Copyright (c) 2011-2020 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. -// ReSharper disable once CheckNamespace namespace DynamicData { /// - /// A simple adaptor to inject side effects into a changeset observable + /// A simple adaptor to inject side effects into a change set observable. /// /// The type of the object. public interface IChangeSetAdaptor @@ -14,4 +16,4 @@ public interface IChangeSetAdaptor /// The change. void Adapt(IChangeSet change); } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/IExtendedList.cs b/src/DynamicData/List/IExtendedList.cs index b31b107a1..928257ee0 100644 --- a/src/DynamicData/List/IExtendedList.cs +++ b/src/DynamicData/List/IExtendedList.cs @@ -1,45 +1,46 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. +using System; using System.Collections.Generic; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Represents a list which supports range operations + /// Represents a list which supports range operations. /// - /// + /// The type of the item. public interface IExtendedList : IList { /// /// Adds the elements of the specified collection to the end of the collection. /// - /// The items to add - /// is null. + /// The items to add. + /// is null. void AddRange(IEnumerable collection); /// - /// Inserts the elements of a collection into the at the specified index. + /// Inserts the elements of a collection into the at the specified index. /// - /// The items to insert + /// The items to insert. /// The zero-based index at which the new elements should be inserted. - /// is null. - /// is less than 0.-or- is greater than . + /// is null. + /// is less than 0.-or- is greater than . void InsertRange(IEnumerable collection, int index); /// - /// Removes a range of elements from the . - /// - /// The zero-based starting index of the range of elements to remove.The number of elements to remove. is less than 0.-or- is less than 0. and do not denote a valid range of elements in the . - void RemoveRange(int index, int count); - - /// - /// Moves an item from the original to the destination index + /// Moves an item from the original to the destination index. /// /// The original. /// The destination. void Move(int original, int destination); + + /// + /// Removes a range of elements from the . + /// + /// The zero-based starting index of the range of elements to remove.The number of elements to remove. is less than 0.-or- is less than 0. and do not denote a valid range of elements in the . + void RemoveRange(int index, int count); } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/IGroup.cs b/src/DynamicData/List/IGroup.cs index 030e49b7e..29eec0b61 100644 --- a/src/DynamicData/List/IGroup.cs +++ b/src/DynamicData/List/IGroup.cs @@ -1,8 +1,11 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// A grouping of observable lists + /// A grouping of observable lists. /// /// The type of the object. /// The type of the group. @@ -18,4 +21,4 @@ public interface IGroup /// IObservableList List { get; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/IGrouping.cs b/src/DynamicData/List/IGrouping.cs index fc9d5fd53..d1956d395 100644 --- a/src/DynamicData/List/IGrouping.cs +++ b/src/DynamicData/List/IGrouping.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,16 +7,16 @@ namespace DynamicData.List { /// - /// Represents a group which provides an update after any value within the group changes + /// Represents a group which provides an update after any value within the group changes. /// /// The type of the object. /// The type of the group key. - public interface IGrouping + public interface IGrouping { /// - /// Gets the group key + /// Gets the count. /// - TGroupKey Key { get; } + int Count { get; } /// /// Gets the items. @@ -24,9 +24,8 @@ public interface IGrouping IEnumerable Items { get; } /// - /// Gets the count. + /// Gets the group key. /// - int Count { get; } - + TGroupKey Key { get; } } } \ No newline at end of file diff --git a/src/DynamicData/List/IObservableList.cs b/src/DynamicData/List/IObservableList.cs index 2045f48eb..1c7a38101 100644 --- a/src/DynamicData/List/IObservableList.cs +++ b/src/DynamicData/List/IObservableList.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,37 +10,40 @@ namespace DynamicData { /// /// A readonly observable list, providing observable methods - /// as well as data access methods + /// as well as data access methods. /// + /// The type of item. public interface IObservableList : IDisposable { /// - /// Connect to the observable list and observe any changes - /// starting with the list's initial items. + /// Gets the count. /// - /// The result will be filtered on the specified predicate. - IObservable> Connect(Func predicate = null); + int Count { get; } /// - /// Connect to the observable list and observe any changes before they are applied to the list. - /// Unlike Connect(), the returned observable is not prepended with the lists initial items. + /// Gets observe the count changes, starting with the initial items count. /// - /// The result will be filtered on the specified predicate. - IObservable> Preview(Func predicate = null); + IObservable CountChanged { get; } /// - /// Observe the count changes, starting with the inital items count + /// Gets items enumerable. /// - IObservable CountChanged { get; } + IEnumerable Items { get; } /// - /// Items enumerable + /// Connect to the observable list and observe any changes + /// starting with the list's initial items. /// - IEnumerable Items { get; } + /// The result will be filtered on the specified predicate. + /// An observable which emits the change set. + IObservable> Connect(Func? predicate = null); /// - /// Gets the count. + /// Connect to the observable list and observe any changes before they are applied to the list. + /// Unlike Connect(), the returned observable is not prepended with the lists initial items. /// - int Count { get; } + /// The result will be filtered on the specified predicate. + /// An observable which emits the change set. + IObservable> Preview(Func? predicate = null); } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/IPageChangeSet.cs b/src/DynamicData/List/IPageChangeSet.cs index 73996336d..a31f32cab 100644 --- a/src/DynamicData/List/IPageChangeSet.cs +++ b/src/DynamicData/List/IPageChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -15,7 +15,7 @@ namespace DynamicData public interface IPageChangeSet : IChangeSet { /// - /// The parameters used to virtualise the stream + /// Gets the parameters used to virtualise the stream. /// IPageResponse Response { get; } } diff --git a/src/DynamicData/List/ISourceList.cs b/src/DynamicData/List/ISourceList.cs index ad2a80ddc..a75de4ac5 100644 --- a/src/DynamicData/List/ISourceList.cs +++ b/src/DynamicData/List/ISourceList.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,15 +9,15 @@ namespace DynamicData { /// /// An editable observable list, providing observable methods - /// as well as data access methods + /// as well as data access methods. /// - /// + /// The type of the item. public interface ISourceList : IObservableList { /// - /// Edit the inner list within the list's internal locking mechanism + /// Edit the inner list within the list's internal locking mechanism. /// /// The update action. void Edit(Action> updateAction); } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/IVirtualChangeSet.cs b/src/DynamicData/List/IVirtualChangeSet.cs index 9af4f3f86..14ff897ca 100644 --- a/src/DynamicData/List/IVirtualChangeSet.cs +++ b/src/DynamicData/List/IVirtualChangeSet.cs @@ -1,4 +1,7 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// @@ -9,7 +12,7 @@ namespace DynamicData public interface IVirtualChangeSet : IChangeSet { /// - /// The parameters used to virtualise the stream + /// Gets the parameters used to virtualise the stream. /// IVirtualResponse Response { get; } } diff --git a/src/DynamicData/List/Internal/AnonymousObservableList.cs b/src/DynamicData/List/Internal/AnonymousObservableList.cs index 757fc01a8..b0ee12e6f 100644 --- a/src/DynamicData/List/Internal/AnonymousObservableList.cs +++ b/src/DynamicData/List/Internal/AnonymousObservableList.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -13,7 +13,7 @@ internal sealed class AnonymousObservableList : IObservableList public AnonymousObservableList(IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -26,16 +26,16 @@ public AnonymousObservableList(ISourceList sourceList) _sourceList = sourceList ?? throw new ArgumentNullException(nameof(sourceList)); } + public int Count => _sourceList.Count; + public IObservable CountChanged => _sourceList.CountChanged; public IEnumerable Items => _sourceList.Items; - public int Count => _sourceList.Count; - - public IObservable> Connect(Func predicate = null) => _sourceList.Connect(predicate); - - public IObservable> Preview(Func predicate = null) => _sourceList.Preview(predicate); + public IObservable> Connect(Func? predicate = null) => _sourceList.Connect(predicate); public void Dispose() => _sourceList.Dispose(); + + public IObservable> Preview(Func? predicate = null) => _sourceList.Preview(predicate); } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/AutoRefresh.cs b/src/DynamicData/List/Internal/AutoRefresh.cs index 964e0f079..7f2293929 100644 --- a/src/DynamicData/List/Internal/AutoRefresh.cs +++ b/src/DynamicData/List/Internal/AutoRefresh.cs @@ -1,78 +1,67 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; -using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal class AutoRefresh { - private readonly IObservable> _source; - private readonly Func> _reevaluator; private readonly TimeSpan? _buffer; - private readonly IScheduler _scheduler; - public AutoRefresh(IObservable> source, - Func> reevaluator, - TimeSpan? buffer = null, - IScheduler scheduler = null) + private readonly Func> _reEvaluator; + + private readonly IScheduler? _scheduler; + + private readonly IObservable> _source; + + public AutoRefresh(IObservable> source, Func> reEvaluator, TimeSpan? buffer = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); - _reevaluator = reevaluator ?? throw new ArgumentNullException(nameof(reevaluator)); + _reEvaluator = reEvaluator ?? throw new ArgumentNullException(nameof(reEvaluator)); _buffer = buffer; _scheduler = scheduler; } public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); + return Observable.Create>( + observer => + { + var locker = new object(); - var allItems = new List(); + var allItems = new List(); - var shared = _source - .Synchronize(locker) - .Clone(allItems) //clone all items so we can look up the index when a change has been made - .Publish(); + var shared = _source.Synchronize(locker).Clone(allItems) // clone all items so we can look up the index when a change has been made + .Publish(); - //monitor each item observable and create change - var itemHasChanged = shared.MergeMany((t) => _reevaluator(t).Select(x => t)); + // monitor each item observable and create change + var itemHasChanged = shared.MergeMany((t) => _reEvaluator(t).Select(_ => t)); - //create a changeset, either buffered or one item at the time - IObservable> itemsChanged; - if (_buffer == null) - { - itemsChanged = itemHasChanged.Select(t => new[] {t}); - } - else - { - itemsChanged = itemHasChanged.Buffer(_buffer.Value, _scheduler ?? Scheduler.Default) - .Where(list => list.Any()); - } + // create a change set, either buffered or one item at the time + IObservable> itemsChanged = _buffer is null ? + itemHasChanged.Select(t => new[] { t }) : + itemHasChanged.Buffer(_buffer.Value, _scheduler ?? Scheduler.Default).Where(list => list.Count > 0); - IObservable> requiresRefresh = itemsChanged - .Synchronize(locker) - .Select(items => - { - //catch all the indices of items which have been refreshed - return allItems.IndexOfMany(items, (t, idx) => new Change(ListChangeReason.Refresh, t, idx)); - }).Select(changes => new ChangeSet(changes)); + IObservable> requiresRefresh = itemsChanged.Synchronize(locker).Select( + items => + { + // catch all the indices of items which have been refreshed + return allItems.IndexOfMany(items, (t, idx) => new Change(ListChangeReason.Refresh, t, idx)); + }).Select(changes => new ChangeSet(changes)); - //publish refreshes and underlying changes - var publisher = shared - .Merge(requiresRefresh) - .SubscribeSafe(observer); + // publish refreshes and underlying changes + var publisher = shared.Merge(requiresRefresh).SubscribeSafe(observer); - return new CompositeDisposable(publisher, shared.Connect()); - }); + return new CompositeDisposable(publisher, shared.Connect()); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/BufferIf.cs b/src/DynamicData/List/Internal/BufferIf.cs index 7af1194a2..496ead77a 100644 --- a/src/DynamicData/List/Internal/BufferIf.cs +++ b/src/DynamicData/List/Internal/BufferIf.cs @@ -1,27 +1,28 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; -using System.Collections.Generic; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; -using DynamicData.Annotations; namespace DynamicData.List.Internal { - internal sealed class BufferIf + internal sealed class BufferIf { - private readonly IObservable> _source; - private readonly IObservable _pauseIfTrueSelector; private readonly bool _initialPauseState; - private readonly TimeSpan _timeOut; + + private readonly IObservable _pauseIfTrueSelector; + private readonly IScheduler _scheduler; - public BufferIf([NotNull] IObservable> source, [NotNull] IObservable pauseIfTrueSelector, - bool initialPauseState = false, TimeSpan? timeOut = null, IScheduler scheduler = null) + private readonly IObservable> _source; + + private readonly TimeSpan _timeOut; + + public BufferIf(IObservable> source, IObservable pauseIfTrueSelector, bool initialPauseState = false, TimeSpan? timeOut = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); _pauseIfTrueSelector = pauseIfTrueSelector ?? throw new ArgumentNullException(nameof(pauseIfTrueSelector)); @@ -32,9 +33,8 @@ public BufferIf([NotNull] IObservable> source, [NotNull] IObservab public IObservable> Run() { - return Observable.Create> - ( - observer => + return Observable.Create>( + observer => { var locker = new object(); var paused = _initialPauseState; @@ -42,67 +42,64 @@ public IObservable> Run() var timeoutSubscriber = new SerialDisposable(); var timeoutSubject = new Subject(); - var bufferSelector = Observable.Return(_initialPauseState) - .Concat(_pauseIfTrueSelector.Merge(timeoutSubject)) - .ObserveOn(_scheduler) - .Synchronize(locker) - .Publish(); - - var pause = bufferSelector.Where(state => state) - .Subscribe(_ => - { - paused = true; - //add pause timeout if required - if (_timeOut != TimeSpan.Zero) - { - timeoutSubscriber.Disposable = Observable.Timer(_timeOut, _scheduler) - .Select(l => false) - .SubscribeSafe(timeoutSubject); - } - }); - - var resume = bufferSelector.Where(state => !state) - .Subscribe(_ => - { - paused = false; - //publish changes and clear buffer - if (buffer.Count == 0) - { - return; - } - - observer.OnNext(buffer); - buffer = new ChangeSet(); - - //kill off timeout if required - timeoutSubscriber.Disposable = Disposable.Empty; - }); - - var updateSubscriber = _source.Synchronize(locker) - .Subscribe(updates => - { - if (paused) - { - buffer.AddRange(updates); - } - else - { - observer.OnNext(updates); - } - }); + var bufferSelector = Observable.Return(_initialPauseState).Concat(_pauseIfTrueSelector.Merge(timeoutSubject)).ObserveOn(_scheduler).Synchronize(locker).Publish(); + + var pause = bufferSelector.Where(state => state).Subscribe( + _ => + { + paused = true; + + // add pause timeout if required + if (_timeOut != TimeSpan.Zero) + { + timeoutSubscriber.Disposable = Observable.Timer(_timeOut, _scheduler).Select(_ => false).SubscribeSafe(timeoutSubject); + } + }); + + var resume = bufferSelector.Where(state => !state).Subscribe( + _ => + { + paused = false; + + // publish changes and clear buffer + if (buffer.Count == 0) + { + return; + } + + observer.OnNext(buffer); + buffer = new ChangeSet(); + + // kill off timeout if required + timeoutSubscriber.Disposable = Disposable.Empty; + }); + + var updateSubscriber = _source.Synchronize(locker).Subscribe( + updates => + { + if (paused) + { + buffer.AddRange(updates); + } + else + { + observer.OnNext(updates); + } + }); var connected = bufferSelector.Connect(); - return Disposable.Create(() => - { - connected.Dispose(); - pause.Dispose(); - resume.Dispose(); - updateSubscriber.Dispose(); - timeoutSubject.OnCompleted(); - timeoutSubscriber.Dispose(); - }); + return Disposable.Create( + () => + { + connected.Dispose(); + pause.Dispose(); + resume.Dispose(); + updateSubscriber.Dispose(); + timeoutSubject.OnCompleted(); + timeoutSubscriber.Dispose(); + }); }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/Combiner.cs b/src/DynamicData/List/Internal/Combiner.cs index f0d03e7c0..4802f12d6 100644 --- a/src/DynamicData/List/Internal/Combiner.cs +++ b/src/DynamicData/List/Internal/Combiner.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,18 +7,20 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Cache.Internal; namespace DynamicData.List.Internal { internal sealed class Combiner { - private readonly object _locker = new object(); + private readonly object _locker = new(); + private readonly ICollection>> _source; + private readonly CombineOperator _type; - public Combiner([NotNull] ICollection>> source, CombineOperator type) + public Combiner(ICollection>> source, CombineOperator type) { _source = source ?? throw new ArgumentNullException(nameof(source)); _type = type; @@ -26,35 +28,36 @@ public Combiner([NotNull] ICollection>> source, Combin public IObservable> Run() { - return Observable.Create>(observer => - { - var disposable = new CompositeDisposable(); - - var resultList = new ChangeAwareListWithRefCounts(); + return Observable.Create>( + observer => + { + var disposable = new CompositeDisposable(); - lock (_locker) - { - var sourceLists = Enumerable.Range(0, _source.Count) - .Select(_ => new ReferenceCountTracker()) - .ToList(); + var resultList = new ChangeAwareListWithRefCounts(); - foreach (var pair in _source.Zip(sourceLists, (item, list) => new { Item = item, List = list })) - { - disposable.Add(pair.Item.Synchronize(_locker).Subscribe(changes => + lock (_locker) { - CloneSourceList(pair.List, changes); + var sourceLists = Enumerable.Range(0, _source.Count).Select(_ => new ReferenceCountTracker()).ToList(); - var notifications = UpdateResultList(changes, sourceLists, resultList); - if (notifications.Count != 0) + foreach (var pair in _source.Zip(sourceLists, (item, list) => new { Item = item, List = list })) { - observer.OnNext(notifications); + disposable.Add( + pair.Item.Synchronize(_locker).Subscribe( + changes => + { + CloneSourceList(pair.List, changes); + + var notifications = UpdateResultList(changes, sourceLists, resultList); + if (notifications.Count != 0) + { + observer.OnNext(notifications); + } + })); } - })); - } - } + } - return disposable; - }); + return disposable; + }); } private static void CloneSourceList(ReferenceCountTracker tracker, IChangeSet changes) @@ -66,6 +69,7 @@ private static void CloneSourceList(ReferenceCountTracker tracker, IChangeSet case ListChangeReason.Add: tracker.Add(change.Item.Current); break; + case ListChangeReason.AddRange: foreach (var t in change.Range) { @@ -73,13 +77,16 @@ private static void CloneSourceList(ReferenceCountTracker tracker, IChangeSet } break; + case ListChangeReason.Replace: tracker.Remove(change.Item.Previous.Value); tracker.Add(change.Item.Current); break; + case ListChangeReason.Remove: tracker.Remove(change.Item.Current); break; + case ListChangeReason.RemoveRange: case ListChangeReason.Clear: foreach (var t in change.Range) @@ -92,9 +99,54 @@ private static void CloneSourceList(ReferenceCountTracker tracker, IChangeSet } } + private bool MatchesConstraint(List> sourceLists, T item) + { + switch (_type) + { + case CombineOperator.And: + { + return sourceLists.All(s => s.Contains(item)); + } + + case CombineOperator.Or: + { + return sourceLists.Any(s => s.Contains(item)); + } + + case CombineOperator.Xor: + { + return sourceLists.Count(s => s.Contains(item)) == 1; + } + + case CombineOperator.Except: + { + var first = sourceLists[0].Contains(item); + var others = sourceLists.Skip(1).Any(s => s.Contains(item)); + return first && !others; + } + + default: + throw new ArgumentOutOfRangeException(nameof(item)); + } + } + + private void UpdateItemMembership(T item, List> sourceLists, ChangeAwareListWithRefCounts resultList) + { + var isInResult = resultList.Contains(item); + var shouldBeInResult = MatchesConstraint(sourceLists, item); + if (shouldBeInResult && !isInResult) + { + resultList.Add(item); + } + else if (!shouldBeInResult && isInResult) + { + resultList.Remove(item); + } + } + private IChangeSet UpdateResultList(IChangeSet changes, List> sourceLists, ChangeAwareListWithRefCounts resultList) { - //child caches have been updated before we reached this point. + // child caches have been updated before we reached this point. foreach (var change in changes.Flatten()) { switch (change.Reason) @@ -114,14 +166,14 @@ private IChangeSet UpdateResultList(IChangeSet changes, List UpdateResultList(IChangeSet changes, List> sourceLists, ChangeAwareListWithRefCounts resultList) - { - var isInResult = resultList.Contains(item); - var shouldBeInResult = MatchesConstraint(sourceLists, item); - if (shouldBeInResult && !isInResult) - { - resultList.Add(item); - } - else if (!shouldBeInResult && isInResult) - { - resultList.Remove(item); - } - } - - private bool MatchesConstraint(List> sourceLists, T item) - { - switch (_type) - { - case CombineOperator.And: - { - return sourceLists.All(s => s.Contains(item)); - } - - case CombineOperator.Or: - { - return sourceLists.Any(s => s.Contains(item)); - } - - case CombineOperator.Xor: - { - return sourceLists.Count(s => s.Contains(item)) == 1; - } - - case CombineOperator.Except: - { - var first = sourceLists[0].Contains(item); - var others = sourceLists.Skip(1).Any(s => s.Contains(item)); - return first && !others; - } - - default: - throw new ArgumentOutOfRangeException(nameof(item)); - } - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/DeferUntilLoaded.cs b/src/DynamicData/List/Internal/DeferUntilLoaded.cs index f87e5c84e..9d8a40647 100644 --- a/src/DynamicData/List/Internal/DeferUntilLoaded.cs +++ b/src/DynamicData/List/Internal/DeferUntilLoaded.cs @@ -1,10 +1,10 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal @@ -13,19 +13,14 @@ internal class DeferUntilLoaded { private readonly IObservable> _source; - public DeferUntilLoaded([NotNull] IObservable> source) + public DeferUntilLoaded(IObservable> source) { _source = source ?? throw new ArgumentNullException(nameof(source)); } public IObservable> Run() { - return _source.MonitorStatus() - .Where(status => status == ConnectionStatus.Loaded) - .Take(1) - .Select(_ => new ChangeSet()) - .Concat(_source) - .NotEmpty(); + return _source.MonitorStatus().Where(status => status == ConnectionStatus.Loaded).Take(1).Select(_ => new ChangeSet()).Concat(_source).NotEmpty(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/Distinct.cs b/src/DynamicData/List/Internal/Distinct.cs index 79853928e..3fb198225 100644 --- a/src/DynamicData/List/Internal/Distinct.cs +++ b/src/DynamicData/List/Internal/Distinct.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,18 +6,19 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class Distinct + where TValue : notnull { private readonly IObservable> _source; + private readonly Func _valueSelector; - public Distinct([NotNull] IObservable> source, - [NotNull] Func valueSelector) + public Distinct(IObservable> source, Func valueSelector) { _source = source ?? throw new ArgumentNullException(nameof(source)); _valueSelector = valueSelector ?? throw new ArgumentNullException(nameof(valueSelector)); @@ -25,32 +26,32 @@ public Distinct([NotNull] IObservable> source, public IObservable> Run() { - return Observable.Create>(observer => - { - var valueCounters = new Dictionary(); - var result = new ChangeAwareList(); - - return _source.Transform((t, previous,idx ) => + return Observable.Create>( + observer => { - var previousValue = previous.ConvertOr(p => p.Value, () => default(TValue)); - - return new ItemWithMatch(t, _valueSelector(t), previousValue); - },true) - .Select(changes => Process(valueCounters, result, changes)) - .NotEmpty() - .SubscribeSafe(observer); - }); + var valueCounters = new Dictionary(); + var result = new ChangeAwareList(); + + return _source.Transform( + (t, previous, _) => + { + var previousValue = previous.ConvertOr(p => p is null ? default : p.Value, () => default); + + return new ItemWithMatch(t, _valueSelector(t), previousValue); + }, + true).Select(changes => Process(valueCounters, result, changes)).NotEmpty().SubscribeSafe(observer); + }); } private static IChangeSet Process(Dictionary values, ChangeAwareList result, IChangeSet changes) { - void AddAction(TValue value) => values.Lookup(value) - .IfHasValue(count => values[value] = count + 1) - .Else(() => - { - values[value] = 1; - result.Add(value); - }); + void AddAction(TValue value) => + values.Lookup(value).IfHasValue(count => values[value] = count + 1).Else( + () => + { + values[value] = 1; + result.Add(value); + }); void RemoveAction(TValue value) { @@ -60,7 +61,7 @@ void RemoveAction(TValue value) return; } - //decrement counter + // decrement counter var newCount = counter.Value - 1; values[value] = newCount; if (newCount != 0) @@ -68,7 +69,7 @@ void RemoveAction(TValue value) return; } - //if there are none, then remove and notify + // if there are none, then remove and notify result.Remove(value); values.Remove(value); } @@ -79,7 +80,6 @@ void RemoveAction(TValue value) { case ListChangeReason.Add: { - var value = change.Item.Current.Value; AddAction(value); break; @@ -92,17 +92,21 @@ void RemoveAction(TValue value) } case ListChangeReason.Refresh: - { - var value = change.Item.Current.Value; - var previous = change.Item.Current.Previous; - if (value.Equals(previous)) + { + var value = change.Item.Current.Value; + var previous = change.Item.Current.Previous; + if (value.Equals(previous)) { continue; } - RemoveAction(previous); - AddAction(value); - break; + if (previous is not null) + { + RemoveAction(previous); + } + + AddAction(value); + break; } case ListChangeReason.Replace: @@ -146,20 +150,38 @@ void RemoveAction(TValue value) private sealed class ItemWithMatch : IEquatable { - public T Item { get; } - public TValue Value { get; } - public TValue Previous { get; } - - public ItemWithMatch(T item, TValue value, TValue previousValue) + public ItemWithMatch(T item, TValue value, TValue? previousValue) { Item = item; Value = value; Previous = previousValue; } - #region Equality + public T Item { get; } + + public TValue? Previous { get; } - public bool Equals(ItemWithMatch other) + public TValue Value { get; } + + /// Returns a value that indicates whether the values of two objects are equal. + /// The first value to compare. + /// The second value to compare. + /// true if the and parameters have the same value; otherwise, false. + public static bool operator ==(ItemWithMatch left, ItemWithMatch right) + { + return Equals(left, right); + } + + /// Returns a value that indicates whether two objects have different values. + /// The first value to compare. + /// The second value to compare. + /// true if and are not equal; otherwise, false. + public static bool operator !=(ItemWithMatch left, ItemWithMatch right) + { + return !Equals(left, right); + } + + public bool Equals(ItemWithMatch? other) { if (ReferenceEquals(null, other)) { @@ -174,7 +196,7 @@ public bool Equals(ItemWithMatch other) return EqualityComparer.Default.Equals(Item, other.Item); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -196,33 +218,13 @@ public override bool Equals(object obj) public override int GetHashCode() { - return EqualityComparer.Default.GetHashCode(Item); - } - - /// Returns a value that indicates whether the values of two objects are equal. - /// The first value to compare. - /// The second value to compare. - /// true if the and parameters have the same value; otherwise, false. - public static bool operator ==(ItemWithMatch left, ItemWithMatch right) - { - return Equals(left, right); - } - - /// Returns a value that indicates whether two objects have different values. - /// The first value to compare. - /// The second value to compare. - /// true if and are not equal; otherwise, false. - public static bool operator !=(ItemWithMatch left, ItemWithMatch right) - { - return !Equals(left, right); + return Item is null ? 0 : EqualityComparer.Default.GetHashCode(Item); } - #endregion - public override string ToString() { return $"{nameof(Item)}: {Item}, {nameof(Value)}: {Value}, {nameof(Previous)}: {Previous}"; } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/DynamicCombiner.cs b/src/DynamicData/List/Internal/DynamicCombiner.cs index d766b74b1..1ff4ba19f 100644 --- a/src/DynamicData/List/Internal/DynamicCombiner.cs +++ b/src/DynamicData/List/Internal/DynamicCombiner.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,7 +7,7 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Cache.Internal; using DynamicData.Kernel; @@ -15,11 +15,13 @@ namespace DynamicData.List.Internal { internal sealed class DynamicCombiner { + private readonly object _locker = new(); + private readonly IObservableList>> _source; + private readonly CombineOperator _type; - private readonly object _locker = new object(); - public DynamicCombiner([NotNull] IObservableList>> source, CombineOperator type) + public DynamicCombiner(IObservableList>> source, CombineOperator type) { _source = source ?? throw new ArgumentNullException(nameof(source)); _type = type; @@ -27,139 +29,74 @@ public DynamicCombiner([NotNull] IObservableList>> sou public IObservable> Run() { - return Observable.Create>(observer => - { - //this is the resulting list which produces all notifications - var resultList = new ChangeAwareListWithRefCounts(); - - //Transform to a merge container. - //This populates a RefTracker when the original source is subscribed to - var sourceLists = _source.Connect() - .Synchronize(_locker) - .Transform(changeset => new MergeContainer(changeset)) - .AsObservableList(); - - //merge the items back together - var allChanges = sourceLists.Connect() - .MergeMany(mc => mc.Source) - .Synchronize(_locker) - .Subscribe(changes => - { - //Populate result list and check for changes - var notifications = UpdateResultList(sourceLists.Items.AsArray(), resultList, changes); - if (notifications.Count != 0) - { - observer.OnNext(notifications); - } - }); - - //When a list is removed, update all items that were in that list - var removedItem = sourceLists.Connect() - .OnItemRemoved(mc => - { - //Remove items if required - var notifications = UpdateItemSetMemberships(sourceLists.Items.AsArray(), resultList, mc.Tracker.Items); - if (notifications.Count != 0) - { - observer.OnNext(notifications); - } - - //On some operators, items not in the removed list can also be affected. - if (_type == CombineOperator.And || _type == CombineOperator.Except) - { - var itemsToCheck = sourceLists.Items.SelectMany(mc2 => mc2.Tracker.Items).ToArray(); - var notification2 = UpdateItemSetMemberships(sourceLists.Items.AsArray(), resultList, itemsToCheck); - if (notification2.Count != 0) - { - observer.OnNext(notification2); - } - } - }) - .Subscribe(); - - //When a list is added, update all items that are in that list - var sourceChanged = sourceLists.Connect() - .WhereReasonsAre(ListChangeReason.Add, ListChangeReason.AddRange) - .ForEachItemChange(mc => - { - var notifications = UpdateItemSetMemberships(sourceLists.Items.AsArray(), resultList, mc.Current.Tracker.Items); - if (notifications.Count != 0) - { - observer.OnNext(notifications); - } - - //On some operators, items not in the new list can also be affected. - if (_type == CombineOperator.And || _type == CombineOperator.Except) - { - var notification2 = UpdateItemSetMemberships(sourceLists.Items.AsArray(), resultList, resultList.ToArray()); - if (notification2.Count != 0) - { - observer.OnNext(notification2); - } - } - }) - .Subscribe(); - - return new CompositeDisposable(sourceLists, allChanges, removedItem, sourceChanged); - }); - } - - private IChangeSet UpdateResultList(MergeContainer[] sourceLists, ChangeAwareListWithRefCounts resultList, IChangeSet changes) - { - //child caches have been updated before we reached this point. - foreach(var change in changes.Flatten()) - { - switch (change.Reason) - { - case ListChangeReason.Add: - case ListChangeReason.Remove: - UpdateItemMembership(change.Current, sourceLists, resultList); - break; - - case ListChangeReason.Replace: - UpdateItemMembership(change.Previous.Value, sourceLists, resultList); - UpdateItemMembership(change.Current, sourceLists, resultList); - break; - - // Pass through refresh changes: - case ListChangeReason.Refresh: - resultList.Refresh(change.Current); - break; - - // A move does not affect contents and so can be ignored: - case ListChangeReason.Moved: - break; - - // These should not occur as they are replaced by the Flatten operator: - //case ListChangeReason.AddRange: - //case ListChangeReason.RemoveRange: - //case ListChangeReason.Clear: - - default: - throw new ArgumentOutOfRangeException(nameof(change.Reason), "Unsupported change type"); - } - } - return resultList.CaptureChanges(); - } - - private IChangeSet UpdateItemSetMemberships(MergeContainer[] sourceLists, ChangeAwareListWithRefCounts resultingList, IEnumerable items) - { - items.ForEach(item => UpdateItemMembership(item, sourceLists, resultingList)); - return resultingList.CaptureChanges(); - } - - private void UpdateItemMembership(T item, MergeContainer[] sourceLists, ChangeAwareListWithRefCounts resultList) - { - var isInResult = resultList.Contains(item); - var shouldBeInResult = MatchesConstraint(sourceLists, item); - if (shouldBeInResult && !isInResult) - { - resultList.Add(item); - } - else if (!shouldBeInResult && isInResult) - { - resultList.Remove(item); - } + return Observable.Create>( + observer => + { + // this is the resulting list which produces all notifications + var resultList = new ChangeAwareListWithRefCounts(); + + // Transform to a merge container. + // This populates a RefTracker when the original source is subscribed to + var sourceLists = _source.Connect().Synchronize(_locker).Transform(changeSet => new MergeContainer(changeSet)).AsObservableList(); + + // merge the items back together + var allChanges = sourceLists.Connect().MergeMany(mc => mc.Source).Synchronize(_locker).Subscribe( + changes => + { + // Populate result list and check for changes + var notifications = UpdateResultList(sourceLists.Items.AsArray(), resultList, changes); + if (notifications.Count != 0) + { + observer.OnNext(notifications); + } + }); + + // When a list is removed, update all items that were in that list + var removedItem = sourceLists.Connect().OnItemRemoved( + mc => + { + // Remove items if required + var notifications = UpdateItemSetMemberships(sourceLists.Items.AsArray(), resultList, mc.Tracker.Items); + if (notifications.Count != 0) + { + observer.OnNext(notifications); + } + + // On some operators, items not in the removed list can also be affected. + if (_type == CombineOperator.And || _type == CombineOperator.Except) + { + var itemsToCheck = sourceLists.Items.SelectMany(mc2 => mc2.Tracker.Items).ToArray(); + var notification2 = UpdateItemSetMemberships(sourceLists.Items.AsArray(), resultList, itemsToCheck); + if (notification2.Count != 0) + { + observer.OnNext(notification2); + } + } + }).Subscribe(); + + // When a list is added, update all items that are in that list + var sourceChanged = sourceLists.Connect().WhereReasonsAre(ListChangeReason.Add, ListChangeReason.AddRange).ForEachItemChange( + mc => + { + var notifications = UpdateItemSetMemberships(sourceLists.Items.AsArray(), resultList, mc.Current.Tracker.Items); + if (notifications.Count != 0) + { + observer.OnNext(notifications); + } + + // On some operators, items not in the new list can also be affected. + if (_type == CombineOperator.And || _type == CombineOperator.Except) + { + var notification2 = UpdateItemSetMemberships(sourceLists.Items.AsArray(), resultList, resultList.ToArray()); + if (notification2.Count != 0) + { + observer.OnNext(notification2); + } + } + }).Subscribe(); + + return new CompositeDisposable(sourceLists, allChanges, removedItem, sourceChanged); + }); } private bool MatchesConstraint(MergeContainer[] sourceLists, T item) @@ -198,16 +135,76 @@ private bool MatchesConstraint(MergeContainer[] sourceLists, T item) } } - private sealed class MergeContainer + private void UpdateItemMembership(T item, MergeContainer[] sourceLists, ChangeAwareListWithRefCounts resultList) { - public ReferenceCountTracker Tracker { get; } = new ReferenceCountTracker(); - public IObservable> Source { get; } + var isInResult = resultList.Contains(item); + var shouldBeInResult = MatchesConstraint(sourceLists, item); + if (shouldBeInResult && !isInResult) + { + resultList.Add(item); + } + else if (!shouldBeInResult && isInResult) + { + resultList.Remove(item); + } + } + + private IChangeSet UpdateItemSetMemberships(MergeContainer[] sourceLists, ChangeAwareListWithRefCounts resultingList, IEnumerable items) + { + items.ForEach(item => UpdateItemMembership(item, sourceLists, resultingList)); + return resultingList.CaptureChanges(); + } + private IChangeSet UpdateResultList(MergeContainer[] sourceLists, ChangeAwareListWithRefCounts resultList, IChangeSet changes) + { + // child caches have been updated before we reached this point. + foreach (var change in changes.Flatten()) + { + switch (change.Reason) + { + case ListChangeReason.Add: + case ListChangeReason.Remove: + UpdateItemMembership(change.Current, sourceLists, resultList); + break; + + case ListChangeReason.Replace: + UpdateItemMembership(change.Previous.Value, sourceLists, resultList); + UpdateItemMembership(change.Current, sourceLists, resultList); + break; + + // Pass through refresh changes: + case ListChangeReason.Refresh: + resultList.Refresh(change.Current); + break; + + // A move does not affect contents and so can be ignored: + case ListChangeReason.Moved: + break; + + // These should not occur as they are replaced by the Flatten operator: + //// case ListChangeReason.AddRange: + //// case ListChangeReason.RemoveRange: + //// case ListChangeReason.Clear: + + default: + throw new ArgumentOutOfRangeException(nameof(change.Reason), "Unsupported change type"); + } + } + + return resultList.CaptureChanges(); + } + + private sealed class MergeContainer + { public MergeContainer(IObservable> source) { Source = source.Do(Clone); } + public IObservable> Source { get; } + + public ReferenceCountTracker Tracker { get; } = new(); + private void Clone(IChangeSet changes) { foreach (var change in changes) @@ -217,6 +214,7 @@ private void Clone(IChangeSet changes) case ListChangeReason.Add: Tracker.Add(change.Item.Current); break; + case ListChangeReason.AddRange: foreach (var t in change.Range) { @@ -224,13 +222,16 @@ private void Clone(IChangeSet changes) } break; + case ListChangeReason.Replace: Tracker.Remove(change.Item.Previous.Value); Tracker.Add(change.Item.Current); break; + case ListChangeReason.Remove: Tracker.Remove(change.Item.Current); break; + case ListChangeReason.RemoveRange: case ListChangeReason.Clear: foreach (var t in change.Range) @@ -244,4 +245,4 @@ private void Clone(IChangeSet changes) } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/EditDiff.cs b/src/DynamicData/List/Internal/EditDiff.cs index 0dcc5603a..f01f22be8 100644 --- a/src/DynamicData/List/Internal/EditDiff.cs +++ b/src/DynamicData/List/Internal/EditDiff.cs @@ -1,21 +1,22 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal class EditDiff { - private readonly ISourceList _source; private readonly IEqualityComparer _equalityComparer; - public EditDiff([NotNull] ISourceList source, IEqualityComparer equalityComparer) + private readonly ISourceList _source; + + public EditDiff(ISourceList source, IEqualityComparer? equalityComparer) { _source = source ?? throw new ArgumentNullException(nameof(source)); _equalityComparer = equalityComparer ?? EqualityComparer.Default; @@ -23,17 +24,18 @@ public EditDiff([NotNull] ISourceList source, IEqualityComparer equalityCo public void Edit(IEnumerable items) { - _source.Edit(innerList => - { - var originalItems = innerList.AsArray(); - var newItems = items.AsArray(); + _source.Edit( + innerList => + { + var originalItems = innerList.AsArray(); + var newItems = items.AsArray(); - var removes = originalItems.Except(newItems, _equalityComparer); - var adds = newItems.Except(originalItems, _equalityComparer); + var removes = originalItems.Except(newItems, _equalityComparer); + var adds = newItems.Except(originalItems, _equalityComparer); - innerList.Remove(removes); - innerList.AddRange(adds); - }); + innerList.Remove(removes); + innerList.AddRange(adds); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/ExpirableItem.cs b/src/DynamicData/List/Internal/ExpirableItem.cs index a5ad5d41c..42bdb3fd2 100644 --- a/src/DynamicData/List/Internal/ExpirableItem.cs +++ b/src/DynamicData/List/Internal/ExpirableItem.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,13 +7,8 @@ namespace DynamicData.List.Internal { - internal sealed class ExpirableItem : IEquatable> { - public TObject Item { get; } - public DateTime ExpireAt { get; } - public long Index { get; } - public ExpirableItem(TObject value, DateTime dateTime, long index) { Item = value; @@ -21,9 +16,23 @@ public ExpirableItem(TObject value, DateTime dateTime, long index) Index = index; } - #region Equality members + public DateTime ExpireAt { get; } + + public long Index { get; } + + public TObject Item { get; } + + public static bool operator ==(ExpirableItem left, ExpirableItem right) + { + return Equals(left, right); + } - public bool Equals(ExpirableItem other) + public static bool operator !=(ExpirableItem left, ExpirableItem right) + { + return !Equals(left, right); + } + + public bool Equals(ExpirableItem? other) { if (ReferenceEquals(null, other)) { @@ -38,7 +47,7 @@ public bool Equals(ExpirableItem other) return EqualityComparer.Default.Equals(Item, other.Item) && ExpireAt.Equals(other.ExpireAt) && Index == other.Index; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -57,28 +66,16 @@ public override int GetHashCode() { unchecked { - var hashCode = EqualityComparer.Default.GetHashCode(Item); + var hashCode = Item is null ? 0 : EqualityComparer.Default.GetHashCode(Item); hashCode = (hashCode * 397) ^ ExpireAt.GetHashCode(); hashCode = (hashCode * 397) ^ Index.GetHashCode(); return hashCode; } } - public static bool operator ==(ExpirableItem left, ExpirableItem right) - { - return Equals(left, right); - } - - public static bool operator !=(ExpirableItem left, ExpirableItem right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"{Item} @ {ExpireAt}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/ExpireAfter.cs b/src/DynamicData/List/Internal/ExpireAfter.cs index 9f8895eb0..c4c706f9a 100644 --- a/src/DynamicData/List/Internal/ExpireAfter.cs +++ b/src/DynamicData/List/Internal/ExpireAfter.cs @@ -1,30 +1,32 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { - [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] internal sealed class ExpireAfter { - private readonly ISourceList _sourceList; private readonly Func _expireAfter; + + private readonly object _locker; + private readonly TimeSpan? _pollingInterval; + private readonly IScheduler _scheduler; - private readonly object _locker; - public ExpireAfter([NotNull] ISourceList sourceList, [NotNull] Func expireAfter, TimeSpan? pollingInterval, [NotNull] IScheduler scheduler, object locker) + private readonly ISourceList _sourceList; + + public ExpireAfter(ISourceList sourceList, Func expireAfter, TimeSpan? pollingInterval, IScheduler scheduler, object locker) { _sourceList = sourceList ?? throw new ArgumentNullException(nameof(sourceList)); _expireAfter = expireAfter ?? throw new ArgumentNullException(nameof(expireAfter)); @@ -35,71 +37,65 @@ public ExpireAfter([NotNull] ISourceList sourceList, [NotNull] Func> Run() { - return Observable.Create>(observer => - { - var dateTime = _scheduler.Now.DateTime; - long orderItemWasAdded = -1; + return Observable.Create>( + observer => + { + var dateTime = _scheduler.Now.DateTime; + long orderItemWasAdded = -1; - var autoRemover = _sourceList.Connect() - .Synchronize(_locker) - .Do(x => dateTime = _scheduler.Now.DateTime) - .Cast(t => - { - var removeAt = _expireAfter(t); - var expireAt = removeAt.HasValue ? dateTime.Add(removeAt.Value) : DateTime.MaxValue; - return new ExpirableItem(t, expireAt, Interlocked.Increment(ref orderItemWasAdded)); - }) - .AsObservableList(); + var autoRemover = _sourceList.Connect().Synchronize(_locker).Do(_ => dateTime = _scheduler.Now.DateTime).Cast( + t => + { + var removeAt = _expireAfter(t); + var expireAt = removeAt.HasValue ? dateTime.Add(removeAt.Value) : DateTime.MaxValue; + return new ExpirableItem(t, expireAt, Interlocked.Increment(ref orderItemWasAdded)); + }).AsObservableList(); - void RemovalAction() - { - try - { - lock (_locker) + void RemovalAction() { - var toRemove = autoRemover.Items.Where(ei => ei.ExpireAt <= _scheduler.Now.DateTime) - .Select(ei => ei.Item) - .ToList(); + try + { + lock (_locker) + { + var toRemove = autoRemover.Items.Where(ei => ei.ExpireAt <= _scheduler.Now.DateTime).Select(ei => ei.Item).ToList(); - observer.OnNext(toRemove); + observer.OnNext(toRemove); + } + } + catch (Exception ex) + { + observer.OnError(ex); + } } - } - catch (Exception ex) - { - observer.OnError(ex); - } - } - var removalSubscription = new SingleAssignmentDisposable(); - if (_pollingInterval.HasValue) - { - // use polling - // ReSharper disable once InconsistentlySynchronizedField - removalSubscription.Disposable = _scheduler.ScheduleRecurringAction(_pollingInterval.Value, RemovalAction); - } - else - { - //create a timer for each distinct time - removalSubscription.Disposable = autoRemover.Connect() - .DistinctValues(ei => ei.ExpireAt) - .SubscribeMany(datetime => - { - // ReSharper disable once InconsistentlySynchronizedField - var expireAt = datetime.Subtract(_scheduler.Now.DateTime); - // ReSharper disable once InconsistentlySynchronizedField - return Observable.Timer(expireAt, _scheduler) - .Take(1) - .Subscribe(_ => RemovalAction()); - }) - .Subscribe(); - } + var removalSubscription = new SingleAssignmentDisposable(); + if (_pollingInterval.HasValue) + { + // use polling + // ReSharper disable once InconsistentlySynchronizedField + removalSubscription.Disposable = _scheduler.ScheduleRecurringAction(_pollingInterval.Value, RemovalAction); + } + else + { + // create a timer for each distinct time + removalSubscription.Disposable = autoRemover.Connect().DistinctValues(ei => ei.ExpireAt).SubscribeMany( + datetime => + { + // ReSharper disable once InconsistentlySynchronizedField + var expireAt = datetime.Subtract(_scheduler.Now.DateTime); + + // ReSharper disable once InconsistentlySynchronizedField + return Observable.Timer(expireAt, _scheduler).Take(1).Subscribe(_ => RemovalAction()); + }).Subscribe(); + } - return Disposable.Create(() => - { - removalSubscription.Dispose(); - autoRemover.Dispose(); - }); - }); + return Disposable.Create( + () => + { + removalSubscription.Dispose(); + autoRemover.Dispose(); + }); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/Filter.cs b/src/DynamicData/List/Internal/Filter.cs index 27df951fb..2298cbf43 100644 --- a/src/DynamicData/List/Internal/Filter.cs +++ b/src/DynamicData/List/Internal/Filter.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal @@ -14,22 +14,21 @@ namespace DynamicData.List.Internal internal class Filter { private readonly ListFilterPolicy _policy; + + private readonly Func? _predicate; + + private readonly IObservable>? _predicates; + private readonly IObservable> _source; - private readonly IObservable> _predicates; - private readonly Func _predicate; - public Filter([NotNull] IObservable> source, - [NotNull] IObservable> predicates, - ListFilterPolicy policy = ListFilterPolicy.CalculateDiff) + public Filter(IObservable> source, IObservable> predicates, ListFilterPolicy policy = ListFilterPolicy.CalculateDiff) { _policy = policy; _source = source ?? throw new ArgumentNullException(nameof(source)); _predicates = predicates ?? throw new ArgumentNullException(nameof(predicates)); } - public Filter([NotNull] IObservable> source, - [NotNull] Func predicate, - ListFilterPolicy policy = ListFilterPolicy.CalculateDiff) + public Filter(IObservable> source, Func predicate, ListFilterPolicy policy = ListFilterPolicy.CalculateDiff) { _policy = policy; _source = source ?? throw new ArgumentNullException(nameof(source)); @@ -38,174 +37,172 @@ public Filter([NotNull] IObservable> source, public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); + return Observable.Create>( + observer => + { + var locker = new object(); - Func predicate = t => false; - var all = new List(); - var filtered = new ChangeAwareList(); - var immutableFilter = _predicate != null; + Func predicate = _ => false; + var all = new List(); + var filtered = new ChangeAwareList(); + var immutableFilter = _predicate is not null; - IObservable> predicateChanged; + IObservable> predicateChanged; - if (immutableFilter) - { - predicateChanged = Observable.Never>(); - predicate = _predicate; - } - else - { - predicateChanged = _predicates - .Synchronize(locker) - .Select(newPredicate => + if (immutableFilter) { - predicate = newPredicate; - return Requery(predicate, all, filtered); - }); - } - - /* - * Apply the transform operator so 'IsMatch' state can be evaluated and captured one time only - * This is to eliminate the need to re-apply the predicate when determining whether an item was previously matched, - * which is essential when we have mutable state - */ - - //Need to get item by index and store it in the transform - var filteredResult = _source - .Synchronize(locker) - .Transform((t, previous) => - { - var wasMatch = previous.ConvertOr(p => p.IsMatch, () => false); - return new ItemWithMatch(t, predicate(t), wasMatch); - },true) - .Select(changes => - { - //keep track of all changes if filtering on an observable - if (!immutableFilter) + predicateChanged = Observable.Never>(); + predicate = _predicate ?? predicate; + } + else { - all.Clone(changes); + if (_predicates is null) + { + throw new InvalidOperationException("The predicates is not set and the change is not a immutableFilter."); + } + + predicateChanged = _predicates.Synchronize(locker).Select( + newPredicate => + { + predicate = newPredicate; + return Requery(predicate, all, filtered); + }); } - return Process(filtered, changes); - }); + /* + * Apply the transform operator so 'IsMatch' state can be evaluated and captured one time only + * This is to eliminate the need to re-apply the predicate when determining whether an item was previously matched, + * which is essential when we have mutable state + */ - return predicateChanged.Merge(filteredResult) - .NotEmpty() - .Select(changes => changes.Transform(iwm => iwm.Item)) // use convert, not transform + // Need to get item by index and store it in the transform + var filteredResult = _source.Synchronize(locker).Transform( + (t, previous) => + { + var wasMatch = previous.ConvertOr(p => p.IsMatch, () => false); + return new ItemWithMatch(t, predicate(t), wasMatch); + }, + true).Select( + changes => + { + // keep track of all changes if filtering on an observable + if (!immutableFilter) + { + all.Clone(changes); + } + + return Process(filtered, changes); + }); + + return predicateChanged.Merge(filteredResult).NotEmpty().Select(changes => changes.Transform(iwm => iwm.Item)) // use convert, not transform .SubscribeSafe(observer); - }); + }); } private static IChangeSet Process(ChangeAwareList filtered, IChangeSet changes) { - //Maintain all items as well as filtered list. This enables us to a) requery when the predicate changes b) check the previous state when Refresh is called + // Maintain all items as well as filtered list. This enables us to a) re-query when the predicate changes b) check the previous state when Refresh is called foreach (var item in changes) { switch (item.Reason) { case ListChangeReason.Add: - { - var change = item.Item; - if (change.Current.IsMatch) + { + var change = item.Item; + if (change.Current.IsMatch) { filtered.Add(change.Current); } break; - } + } case ListChangeReason.AddRange: - { - var matches = item.Range.Where(t => t.IsMatch).ToList(); - filtered.AddRange(matches); - break; - } + { + var matches = item.Range.Where(t => t.IsMatch).ToList(); + filtered.AddRange(matches); + break; + } case ListChangeReason.Replace: - { - var change = item.Item; - var match = change.Current.IsMatch; - var wasMatch = item.Item.Current.WasMatch; - if (match) { - if (wasMatch) + var change = item.Item; + var match = change.Current.IsMatch; + var wasMatch = item.Item.Current.WasMatch; + if (match) { - //an update, so get the latest index and pass the index up the chain - var previous = filtered.Select(x => x.Item) - .IndexOfOptional(change.Previous.Value.Item) - .ValueOrThrow(() => new InvalidOperationException($"Cannot find index of {typeof(T).Name} -> {change.Previous.Value}. Expected to be in the list")); + if (wasMatch) + { + // an update, so get the latest index and pass the index up the chain + var previous = filtered.Select(x => x.Item).IndexOfOptional(change.Previous.Value.Item).ValueOrThrow(() => new InvalidOperationException($"Cannot find index of {typeof(T).Name} -> {change.Previous.Value}. Expected to be in the list")); - //replace inline + // replace inline filtered[previous.Index] = change.Current; + } + else + { + filtered.Add(change.Current); + } } else { - filtered.Add(change.Current); - } - } - else - { - if (wasMatch) + if (wasMatch) { filtered.Remove(change.Previous.Value); } } - break; - } + break; + } case ListChangeReason.Refresh: - { - var change = item.Item; - var match = change.Current.IsMatch; - var wasMatch = item.Item.Current.WasMatch; - if (match) { - if (wasMatch) + var change = item.Item; + var match = change.Current.IsMatch; + var wasMatch = item.Item.Current.WasMatch; + if (match) { - //an update, so get the latest index and pass the index up the chain - var previous = filtered.Select(x => x.Item) - .IndexOfOptional(change.Current.Item) - .ValueOrThrow(() => new InvalidOperationException($"Cannot find index of {typeof(T).Name} -> {change.Previous.Value}. Expected to be in the list")); + if (wasMatch) + { + // an update, so get the latest index and pass the index up the chain + var previous = filtered.Select(x => x.Item).IndexOfOptional(change.Current.Item).ValueOrThrow(() => new InvalidOperationException($"Cannot find index of {typeof(T).Name} -> {change.Previous.Value}. Expected to be in the list")); - filtered.RefreshAt(previous.Index); + filtered.RefreshAt(previous.Index); + } + else + { + filtered.Add(change.Current); + } } else { - filtered.Add(change.Current); - } - } - else - { - if (wasMatch) + if (wasMatch) { filtered.Remove(change.Current); } } - break; - } + break; + } case ListChangeReason.Remove: - { - filtered.Remove(item.Item.Current); - break; - } + { + filtered.Remove(item.Item.Current); + break; + } case ListChangeReason.RemoveRange: - { - filtered.RemoveMany(item.Range); - break; - } + { + filtered.RemoveMany(item.Range); + break; + } case ListChangeReason.Clear: - { - filtered.ClearOrRemoveMany(item); - break; - } + { + filtered.ClearOrRemoveMany(item); + break; + } } - } return filtered.CaptureChanges(); @@ -222,11 +219,11 @@ private IChangeSet Requery(Func predicate, List new ItemWithMatch(iwm.Item, predicate(iwm.Item), iwm.IsMatch)).ToList(); - //mark items as matched? + // mark items as matched? filtered.Clear(); filtered.AddRange(itemsWithMatch.Where(iwm => iwm.IsMatch)); - //reset state for all items + // reset state for all items all.Clear(); all.AddRange(itemsWithMatch); return filtered.CaptureChanges(); @@ -260,46 +257,21 @@ private IChangeSet Requery(Func predicate, List { - public T Item { get; } - public bool IsMatch { get; } - public bool WasMatch { get; } - public ItemWithMatch(T item, bool isMatch, bool wasMatch = false) - :this() + : this() { Item = item; IsMatch = isMatch; WasMatch = wasMatch; } - #region Equality - - public bool Equals(ItemWithMatch other) - { - return EqualityComparer.Default.Equals(Item, other.Item); - } - - public override bool Equals(object obj) - { - if (obj is null) - { - return false; - } - - if (obj.GetType() != GetType()) - { - return false; - } + public T Item { get; } - return Equals((ItemWithMatch) obj); - } + public bool IsMatch { get; } - public override int GetHashCode() - { - return EqualityComparer.Default.GetHashCode(Item); - } + public bool WasMatch { get; } - /// Returns a value that indicates whether the values of two objects are equal. + /// Returns a value that indicates whether the values of two objects are equal. /// The first value to compare. /// The second value to compare. /// true if the and parameters have the same value; otherwise, false. @@ -308,7 +280,7 @@ public override int GetHashCode() return Equals(left, right); } - /// Returns a value that indicates whether two objects have different values. + /// Returns a value that indicates whether two objects have different values. /// The first value to compare. /// The second value to compare. /// true if and are not equal; otherwise, false. @@ -317,9 +289,22 @@ public override int GetHashCode() return !Equals(left, right); } - #endregion + public bool Equals(ItemWithMatch other) + { + return EqualityComparer.Default.Equals(Item, other.Item); + } + + public override bool Equals(object? obj) + { + return obj is ItemWithMatch value && Equals(value); + } + + public override int GetHashCode() + { + return Item is null ? 0 : EqualityComparer.Default.GetHashCode(Item); + } public override string ToString() => $"{Item}, (was {IsMatch} is {WasMatch}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/FilterOnObservable.cs b/src/DynamicData/List/Internal/FilterOnObservable.cs index b67dfee7a..106e7abe5 100644 --- a/src/DynamicData/List/Internal/FilterOnObservable.cs +++ b/src/DynamicData/List/Internal/FilterOnObservable.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -13,15 +13,15 @@ namespace DynamicData.List.Internal { internal class FilterOnObservable { - private readonly IObservable> _source; - private readonly Func> _filter; private readonly TimeSpan? _buffer; - private readonly IScheduler _scheduler; - public FilterOnObservable(IObservable> source, - Func> filter, - TimeSpan? buffer = null, - IScheduler scheduler = null) + private readonly Func> _filter; + + private readonly IScheduler? _scheduler; + + private readonly IObservable> _source; + + public FilterOnObservable(IObservable> source, Func> filter, TimeSpan? buffer = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); _filter = filter ?? throw new ArgumentNullException(nameof(filter)); @@ -29,9 +29,67 @@ public FilterOnObservable(IObservable> source, _scheduler = scheduler; } + public IObservable> Run() + { + return Observable.Create>( + observer => + { + var locker = new object(); + + var allItems = new List(); + + var shared = _source.Synchronize(locker).Transform(v => new ObjWithFilterValue(v, true)) // we default to true (include all items) + .Clone(allItems) // clone all items so we can look up the index when a change has been made + .Publish(); + + // monitor each item observable and create change, carry the value of the observable property + IObservable itemHasChanged = shared.MergeMany(v => _filter(v.Obj).Select(prop => new ObjWithFilterValue(v.Obj, prop))); + + // create a change set, either buffered or one item at the time + IObservable> itemsChanged = _buffer is null ? + itemHasChanged.Select(t => new[] { t }) : + itemHasChanged.Buffer(_buffer.Value, _scheduler ?? Scheduler.Default).Where(list => list.Count > 0); + + IObservable> requiresRefresh = itemsChanged.Synchronize(locker).Select( + items => + { + // catch all the indices of items which have been refreshed + return IndexOfMany(allItems, items, v => v.Obj, (t, idx) => new Change(ListChangeReason.Refresh, t, idx)); + }).Select(changes => new ChangeSet(changes)); + + // publish refreshes and underlying changes + var publisher = shared.Merge(requiresRefresh).Filter(v => v.Filter).Transform(v => v.Obj).SuppressRefresh() // suppress refreshes from filter, avoids excessive refresh messages for no-op filter updates + .SubscribeSafe(observer); + + return new CompositeDisposable(publisher, shared.Connect()); + }); + } + + private static IEnumerable IndexOfMany(IEnumerable source, IEnumerable itemsToFind, Func objectPropertyFunc, Func resultSelector) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (itemsToFind is null) + { + throw new ArgumentNullException(nameof(itemsToFind)); + } + + if (resultSelector is null) + { + throw new ArgumentNullException(nameof(resultSelector)); + } + + var indexed = source.Select((element, index) => new { Element = element, Index = index }); + return itemsToFind.Join(indexed, objectPropertyFunc, right => objectPropertyFunc(right.Element), (left, right) => resultSelector(left, right.Index)); + } + private readonly struct ObjWithFilterValue : IEquatable { public readonly TObject Obj; + public readonly bool Filter; public ObjWithFilterValue(TObject obj, bool filter) @@ -40,22 +98,6 @@ public ObjWithFilterValue(TObject obj, bool filter) Filter = filter; } - private sealed class ObjEqualityComparer : IEqualityComparer - { - public bool Equals(ObjWithFilterValue x, ObjWithFilterValue y) - { - return EqualityComparer.Default.Equals(x.Obj, y.Obj); - } - - public int GetHashCode(ObjWithFilterValue obj) - { - unchecked - { - return (EqualityComparer.Default.GetHashCode(obj.Obj) * 397); - } - } - } - private static IEqualityComparer ObjComparer { get; } = new ObjEqualityComparer(); public bool Equals(ObjWithFilterValue other) @@ -64,7 +106,7 @@ public bool Equals(ObjWithFilterValue other) return ObjComparer.Equals(this, other); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ObjWithFilterValue value && Equals(value); } @@ -73,87 +115,22 @@ public override int GetHashCode() { return ObjComparer.GetHashCode(this); } - } - public IObservable> Run() - { - return Observable.Create>(observer => + private sealed class ObjEqualityComparer : IEqualityComparer { - var locker = new object(); - - var allItems = new List(); - - var shared = _source - .Synchronize(locker) - // we default to true (include all items) - .Transform(v => new ObjWithFilterValue(v, true)) - .Clone(allItems) //clone all items so we can look up the index when a change has been made - .Publish(); - - //monitor each item observable and create change, carry the value of the observable property - IObservable itemHasChanged = shared.MergeMany(v => - _filter(v.Obj).Select(prop => new ObjWithFilterValue(v.Obj, prop))); - - //create a changeset, either buffered or one item at the time - IObservable> itemsChanged; - if (_buffer == null) - { - itemsChanged = itemHasChanged.Select(t => new[] {t}); - } - else + public bool Equals(ObjWithFilterValue x, ObjWithFilterValue y) { - itemsChanged = itemHasChanged.Buffer(_buffer.Value, _scheduler ?? Scheduler.Default) - .Where(list => list.Any()); + return EqualityComparer.Default.Equals(x.Obj, y.Obj); } - IObservable> requiresRefresh = itemsChanged.Synchronize(locker) - .Select(items => + public int GetHashCode(ObjWithFilterValue obj) + { + unchecked { - //catch all the indices of items which have been refreshed - var indexOfMany = IndexOfMany(allItems, - items, - v => v.Obj, - (t, idx) => new Change(ListChangeReason.Refresh, t, idx)); - return indexOfMany; - }) - .Select(changes => new ChangeSet(changes)); - - //publish refreshes and underlying changes - var publisher = shared - .Merge(requiresRefresh) - .Filter(v => v.Filter) - .Transform(v => v.Obj) - // suppress refreshes from filter, avoids excessive refresh messages for no-op filter updates - .SupressRefresh() - .SubscribeSafe(observer); - - return new CompositeDisposable(publisher, shared.Connect()); - }); - } - - private static IEnumerable IndexOfMany(IEnumerable source, - IEnumerable itemsToFind, - Func objectPropertyFunc, - Func resultSelector) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (itemsToFind == null) - { - throw new ArgumentNullException(nameof(itemsToFind)); - } - - if (resultSelector == null) - { - throw new ArgumentNullException(nameof(resultSelector)); + return (obj.Obj is null ? 0 : EqualityComparer.Default.GetHashCode(obj.Obj)) * 397; + } + } } - - var indexed = source.Select((element, index) => new { Element = element, Index = index }); - return itemsToFind.Join(indexed, objectPropertyFunc, right => objectPropertyFunc(right.Element), - (left, right) => resultSelector(left, right.Index)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/FilterOnProperty.cs b/src/DynamicData/List/Internal/FilterOnProperty.cs index dc82f440d..58d0bcbca 100644 --- a/src/DynamicData/List/Internal/FilterOnProperty.cs +++ b/src/DynamicData/List/Internal/FilterOnProperty.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,15 +10,20 @@ namespace DynamicData.List.Internal { [Obsolete("Use AutoRefresh(), followed by Filter() instead")] - internal class FilterOnProperty where TObject : INotifyPropertyChanged + internal class FilterOnProperty + where TObject : INotifyPropertyChanged { private readonly Func _predicate; - private readonly TimeSpan? _throttle; - private readonly IScheduler _scheduler; + private readonly Expression> _propertySelector; + + private readonly IScheduler? _scheduler; + private readonly IObservable> _source; - public FilterOnProperty(IObservable> source, Expression> propertySelector, Func predicate, TimeSpan? throttle = null, IScheduler scheduler = null) + private readonly TimeSpan? _throttle; + + public FilterOnProperty(IObservable> source, Expression> propertySelector, Func predicate, TimeSpan? throttle = null, IScheduler? scheduler = null) { _source = source; _propertySelector = propertySelector; @@ -29,9 +34,7 @@ public FilterOnProperty(IObservable> source, Expression> Run() { - return _source - .AutoRefresh(_propertySelector, propertyChangeThrottle: _throttle, scheduler: _scheduler) - .Filter(_predicate); + return _source.AutoRefresh(_propertySelector, propertyChangeThrottle: _throttle, scheduler: _scheduler).Filter(_predicate); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/FilterStatic.cs b/src/DynamicData/List/Internal/FilterStatic.cs index 159f25e96..e0f6371bb 100644 --- a/src/DynamicData/List/Internal/FilterStatic.cs +++ b/src/DynamicData/List/Internal/FilterStatic.cs @@ -1,8 +1,7 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. -using DynamicData.Annotations; using System; using System.Linq; using System.Reactive.Linq; @@ -11,11 +10,11 @@ namespace DynamicData.List.Internal { internal class FilterStatic { - private readonly IObservable> _source; private readonly Func _predicate; - public FilterStatic([NotNull] IObservable> source, - [NotNull] Func predicate) + private readonly IObservable> _source; + + public FilterStatic(IObservable> source, Func predicate) { _source = source ?? throw new ArgumentNullException(nameof(source)); _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); @@ -23,13 +22,13 @@ public FilterStatic([NotNull] IObservable> source, public IObservable> Run() { - return _source.Scan(new ChangeAwareList(), (state, changes) => - { - Process(state, changes); - return state; - }) - .Select(filtered => filtered.CaptureChanges()) - .NotEmpty(); + return _source.Scan( + new ChangeAwareList(), + (state, changes) => + { + Process(state, changes); + return state; + }).Select(filtered => filtered.CaptureChanges()).NotEmpty(); } private void Process(ChangeAwareList filtered, IChangeSet changes) @@ -39,58 +38,58 @@ private void Process(ChangeAwareList filtered, IChangeSet changes) switch (item.Reason) { case ListChangeReason.Add: - { - var change = item.Item; - if (_predicate(change.Current)) + { + var change = item.Item; + if (_predicate(change.Current)) { filtered.Add(change.Current); } break; - } + } case ListChangeReason.AddRange: - { - var matches = item.Range.Where(t => _predicate(t)).ToList(); - filtered.AddRange(matches); - break; - } - - case ListChangeReason.Replace: - { - var change = item.Item; - var match = _predicate(change.Current); - if (match) { - filtered.ReplaceOrAdd(change.Previous.Value, change.Current); + var matches = item.Range.Where(t => _predicate(t)).ToList(); + filtered.AddRange(matches); + break; } - else + + case ListChangeReason.Replace: { - filtered.Remove(change.Previous.Value); - } + var change = item.Item; + var match = _predicate(change.Current); + if (match) + { + filtered.ReplaceOrAdd(change.Previous.Value, change.Current); + } + else + { + filtered.Remove(change.Previous.Value); + } - break; - } + break; + } case ListChangeReason.Remove: - { - filtered.Remove(item.Item.Current); - break; - } + { + filtered.Remove(item.Item.Current); + break; + } case ListChangeReason.RemoveRange: - { - filtered.RemoveMany(item.Range); - break; - } + { + filtered.RemoveMany(item.Range); + break; + } case ListChangeReason.Clear: - { - filtered.ClearOrRemoveMany(item); - break; - } + { + filtered.ClearOrRemoveMany(item); + break; + } } } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/Group.cs b/src/DynamicData/List/Internal/Group.cs index 5ec92e970..743e7cc06 100644 --- a/src/DynamicData/List/Internal/Group.cs +++ b/src/DynamicData/List/Internal/Group.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,70 +9,53 @@ namespace DynamicData.List.Internal { internal class Group : IGroup, IDisposable, IEquatable> { - private ISourceList Source { get; } = new SourceList(); + public Group(TGroup groupKey) => GroupKey = groupKey; public TGroup GroupKey { get; } + public IObservableList List => Source; - public Group(TGroup groupKey) => GroupKey = groupKey; - public void Edit(Action> editAction) => Source.Edit(editAction); + private ISourceList Source { get; } = new SourceList(); - #region Equality + public static bool operator ==(Group left, Group right) + { + return Equals(left, right); + } - public bool Equals(Group other) + public static bool operator !=(Group left, Group right) { - if (ReferenceEquals(null, other)) - { - return false; - } + return !Equals(left, right); + } - if (ReferenceEquals(this, other)) - { - return true; - } + public void Dispose() => Source.Dispose(); - return EqualityComparer.Default.Equals(GroupKey, other.GroupKey); - } + public void Edit(Action> editAction) => Source.Edit(editAction); - public override bool Equals(object obj) + public bool Equals(Group? other) { - if (ReferenceEquals(null, obj)) + if (ReferenceEquals(null, other)) { return false; } - if (ReferenceEquals(this, obj)) + if (ReferenceEquals(this, other)) { return true; } - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((Group)obj); - } - - public override int GetHashCode() - { - return EqualityComparer.Default.GetHashCode(GroupKey); + return EqualityComparer.Default.Equals(GroupKey, other.GroupKey); } - public static bool operator ==(Group left, Group right) + public override bool Equals(object? obj) { - return Equals(left, right); + return obj is Group value && Equals(value); } - public static bool operator !=(Group left, Group right) + public override int GetHashCode() { - return !Equals(left, right); + return GroupKey is null ? 0 : EqualityComparer.Default.GetHashCode(GroupKey); } - #endregion - public override string ToString() => $"Group of {GroupKey} ({List.Count} records)"; - - public void Dispose() => Source.Dispose(); } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/GroupOn.cs b/src/DynamicData/List/Internal/GroupOn.cs index 9c2db2eab..f8fd4cb8e 100644 --- a/src/DynamicData/List/Internal/GroupOn.cs +++ b/src/DynamicData/List/Internal/GroupOn.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,112 +8,72 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class GroupOn + where TGroupKey : notnull { - private readonly IObservable> _source; private readonly Func _groupSelector; - private readonly IObservable _regrouper; - public GroupOn([NotNull] IObservable> source, [NotNull] Func groupSelector, IObservable regrouper) + private readonly IObservable? _regrouper; + + private readonly IObservable> _source; + + public GroupOn(IObservable> source, Func groupSelector, IObservable? regrouper) { _source = source ?? throw new ArgumentNullException(nameof(source)); _groupSelector = groupSelector ?? throw new ArgumentNullException(nameof(groupSelector)); - _regrouper = regrouper ; + _regrouper = regrouper; } public IObservable>> Run() { - return Observable.Create>>(observer => - { - var groupings = new ChangeAwareList>(); - var groupCache = new Dictionary>(); - - //capture the grouping up front which has the benefit that the group key is only selected once - var itemsWithGroup = _source - .Transform((t, previous) => + return Observable.Create>>( + observer => { - return new ItemWithGroupKey(t, _groupSelector(t), previous.Convert(p=>p.Group)); - },true); + var groupings = new ChangeAwareList>(); + var groupCache = new Dictionary>(); - var locker = new object(); - var shared = itemsWithGroup.Synchronize(locker).Publish(); + // capture the grouping up front which has the benefit that the group key is only selected once + var itemsWithGroup = _source.Transform((t, previous) => new ItemWithGroupKey(t, _groupSelector(t), previous.Convert(p => p.Group)), true); - var grouper = shared - .Select(changes => Process(groupings, groupCache, changes)); + var locker = new object(); + var shared = itemsWithGroup.Synchronize(locker).Publish(); - IObservable>> regrouper; - if (_regrouper == null) - { - regrouper = Observable.Never>>(); - } - else - { - regrouper = _regrouper.Synchronize(locker) - .CombineLatest(shared.ToCollection(), (_, collection) => Regroup(groupings, groupCache, collection)); - } + var grouper = shared.Select(changes => Process(groupings, groupCache, changes)); + + IObservable>> regrouper = _regrouper is null ? + Observable.Never>>() : + _regrouper.Synchronize(locker).CombineLatest(shared.ToCollection(), (_, collection) => Regroup(groupings, groupCache, collection)); - var publisher = grouper.Merge(regrouper) - .DisposeMany() //dispose removes as the grouping is disposable - .NotEmpty() - .SubscribeSafe(observer); + var publisher = grouper.Merge(regrouper).DisposeMany() // dispose removes as the grouping is disposable + .NotEmpty().SubscribeSafe(observer); - return new CompositeDisposable( publisher, shared.Connect()); - }); + return new CompositeDisposable(publisher, shared.Connect()); + }); } - private IChangeSet> Regroup(ChangeAwareList> result, - IDictionary> groupCollection, - IReadOnlyCollection currentItems) + private static GroupWithAddIndicator GetCache(IDictionary> groupCaches, TGroupKey key) { - //TODO: We need to update ItemWithValue> - - foreach (var itemWithValue in currentItems) + var cache = groupCaches.Lookup(key); + if (cache.HasValue) { - var currentGroupKey = itemWithValue.Group; - var newGroupKey = _groupSelector(itemWithValue.Item); - if (newGroupKey.Equals(currentGroupKey)) - { - continue; - } - - //remove from the old group - var currentGroupLookup = GetCache(groupCollection, currentGroupKey); - var currentGroupCache = currentGroupLookup.Group; - currentGroupCache.Edit(innerList=> innerList.Remove(itemWithValue.Item)); - - if (currentGroupCache.List.Count == 0) - { - groupCollection.Remove(currentGroupKey); - result.Remove(currentGroupCache); - } - - //Mark the old item with the new cache group - itemWithValue.Group = newGroupKey; - - //add to the new group - var newGroupLookup = GetCache(groupCollection, newGroupKey); - var newGroupCache = newGroupLookup.Group; - newGroupCache.Edit(innerList => innerList.Add(itemWithValue.Item)); - - if (newGroupLookup.WasCreated) - { - result.Add(newGroupCache); - } + return new GroupWithAddIndicator(cache.Value, false); } - return result.CaptureChanges(); + var newcache = new Group(key); + groupCaches[key] = newcache; + return new GroupWithAddIndicator(newcache, true); } private static IChangeSet> Process(ChangeAwareList> result, IDictionary> groupCollection, IChangeSet changes) { foreach (var grouping in changes.Unified().GroupBy(change => change.Current.Group)) { - //lookup group and if created, add to result set + // lookup group and if created, add to result set var currentGroup = grouping.Key; var lookup = GetCache(groupCollection, currentGroup); var groupCache = lookup.Group; @@ -123,106 +83,106 @@ private static IChangeSet> Process(ChangeAwareList - { - //iterate through the group's items and process - foreach (var change in grouping) { - switch (change.Reason) + // iterate through the group's items and process + foreach (var change in grouping) { - case ListChangeReason.Add: - { - list.Add(change.Current.Item); - break; - } - - case ListChangeReason.Replace: - { - var previousItem = change.Previous.Value.Item; - var previousGroup = change.Previous.Value.Group; - - //check whether an item changing has resulted in a different group - if (previousGroup.Equals(currentGroup)) + switch (change.Reason) + { + case ListChangeReason.Add: { - //find and replace - var index = list.IndexOf(previousItem); - list[index] = change.Current.Item; + list.Add(change.Current.Item); + break; } - else + + case ListChangeReason.Replace: { - //add to new group - list.Add(change.Current.Item); + var previousItem = change.Previous.Value.Item; + var previousGroup = change.Previous.Value.Group; - //remove from old group - groupCollection.Lookup(previousGroup) - .IfHasValue(g => - { - g.Edit(oldList => oldList.Remove(previousItem)); - if (g.List.Count != 0) - { - return; - } - - groupCollection.Remove(g.GroupKey); - result.Remove(g); - }); + // check whether an item changing has resulted in a different group + if (previousGroup.Equals(currentGroup)) + { + // find and replace + var index = list.IndexOf(previousItem); + list[index] = change.Current.Item; + } + else + { + // add to new group + list.Add(change.Current.Item); + + // remove from old group + groupCollection.Lookup(previousGroup).IfHasValue( + g => + { + g.Edit(oldList => oldList.Remove(previousItem)); + if (g.List.Count != 0) + { + return; + } + + groupCollection.Remove(g.GroupKey); + result.Remove(g); + }); + } + + break; } - break; - } + case ListChangeReason.Refresh: + { + // 1. Check whether item was in the group and should not be now (or vice versa) + var currentItem = change.Current.Item; + var previousGroup = change.Current.PreviousGroup.Value; - case ListChangeReason.Refresh: - { - //1. Check whether item was in the group and should not be now (or vice versa) - var currentItem = change.Current.Item; - var previousGroup = change.Current.PreviousGroup.Value; - - //check whether an item changing has resulted in a different group - if (previousGroup.Equals(currentGroup)) - { - // Propagate refresh event - var cal = (ChangeAwareList) list; - cal.Refresh(currentItem); - } - else - { - //add to new group - list.Add(currentItem); - - //remove from old group if empty - groupCollection.Lookup(previousGroup) - .IfHasValue(g => + // check whether an item changing has resulted in a different group + if (previousGroup.Equals(currentGroup)) { - g.Edit(oldList => oldList.Remove(currentItem)); - if (g.List.Count != 0) - { - return; - } - - groupCollection.Remove(g.GroupKey); - result.Remove(g); - }); - } - - break; - } + // Propagate refresh event + var cal = (ChangeAwareList)list; + cal.Refresh(currentItem); + } + else + { + // add to new group + list.Add(currentItem); + + // remove from old group if empty + groupCollection.Lookup(previousGroup).IfHasValue( + g => + { + g.Edit(oldList => oldList.Remove(currentItem)); + if (g.List.Count != 0) + { + return; + } + + groupCollection.Remove(g.GroupKey); + result.Remove(g); + }); + } + + break; + } + + case ListChangeReason.Remove: + { + list.Remove(change.Current.Item); + break; + } - case ListChangeReason.Remove: - { - list.Remove(change.Current.Item); - break; - } - - case ListChangeReason.Clear: - { - list.Clear(); - break; - } + case ListChangeReason.Clear: + { + list.Clear(); + break; + } + } } - } - }); + }); if (groupCache.List.Count == 0) { @@ -234,38 +194,62 @@ private static IChangeSet> Process(ChangeAwareList> groupCaches, TGroupKey key) + private IChangeSet> Regroup(ChangeAwareList> result, IDictionary> groupCollection, IReadOnlyCollection currentItems) { - var cache = groupCaches.Lookup(key); - if (cache.HasValue) + // TODO: We need to update ItemWithValue> + foreach (var itemWithValue in currentItems) { - return new GroupWithAddIndicator(cache.Value, false); + var currentGroupKey = itemWithValue.Group; + var newGroupKey = _groupSelector(itemWithValue.Item); + if (newGroupKey.Equals(currentGroupKey)) + { + continue; + } + + // remove from the old group + var currentGroupLookup = GetCache(groupCollection, currentGroupKey); + var currentGroupCache = currentGroupLookup.Group; + currentGroupCache.Edit(innerList => innerList.Remove(itemWithValue.Item)); + + if (currentGroupCache.List.Count == 0) + { + groupCollection.Remove(currentGroupKey); + result.Remove(currentGroupCache); + } + + // Mark the old item with the new cache group + itemWithValue.Group = newGroupKey; + + // add to the new group + var newGroupLookup = GetCache(groupCollection, newGroupKey); + var newGroupCache = newGroupLookup.Group; + newGroupCache.Edit(innerList => innerList.Add(itemWithValue.Item)); + + if (newGroupLookup.WasCreated) + { + result.Add(newGroupCache); + } } - var newcache = new Group(key); - groupCaches[key] = newcache; - return new GroupWithAddIndicator(newcache, true); + return result.CaptureChanges(); } private readonly struct GroupWithAddIndicator { - public Group Group { get; } - public bool WasCreated { get; } - public GroupWithAddIndicator(Group group, bool wasCreated) - :this() + : this() { Group = group; WasCreated = wasCreated; } + + public Group Group { get; } + + public bool WasCreated { get; } } private sealed class ItemWithGroupKey : IEquatable { - public TObject Item { get; } - public TGroupKey Group { get; set; } - public Optional PreviousGroup { get; } - public ItemWithGroupKey(TObject item, TGroupKey group, Optional previousGroup) { Item = item; @@ -273,9 +257,31 @@ public ItemWithGroupKey(TObject item, TGroupKey group, Optional previ PreviousGroup = previousGroup; } - #region Equality + public TGroupKey Group { get; set; } + + public TObject Item { get; } + + public Optional PreviousGroup { get; } + + /// Returns a value that indicates whether the values of two objects are equal. + /// The first value to compare. + /// The second value to compare. + /// true if the and parameters have the same value; otherwise, false. + public static bool operator ==(ItemWithGroupKey left, ItemWithGroupKey right) + { + return Equals(left, right); + } + + /// Returns a value that indicates whether two objects have different values. + /// The first value to compare. + /// The second value to compare. + /// true if and are not equal; otherwise, false. + public static bool operator !=(ItemWithGroupKey left, ItemWithGroupKey right) + { + return !Equals(left, right); + } - public bool Equals(ItemWithGroupKey other) + public bool Equals(ItemWithGroupKey? other) { if (ReferenceEquals(null, other)) { @@ -290,7 +296,7 @@ public bool Equals(ItemWithGroupKey other) return EqualityComparer.Default.Equals(Item, other.Item); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -302,35 +308,15 @@ public override bool Equals(object obj) return true; } - return obj is ItemWithGroupKey && Equals((ItemWithGroupKey) obj); + return obj is ItemWithGroupKey value && Equals(value); } public override int GetHashCode() { - return EqualityComparer.Default.GetHashCode(Item); - } - - /// Returns a value that indicates whether the values of two objects are equal. - /// The first value to compare. - /// The second value to compare. - /// true if the and parameters have the same value; otherwise, false. - public static bool operator ==(ItemWithGroupKey left, ItemWithGroupKey right) - { - return Equals(left, right); + return Item is null ? 0 : EqualityComparer.Default.GetHashCode(Item); } - /// Returns a value that indicates whether two objects have different values. - /// The first value to compare. - /// The second value to compare. - /// true if and are not equal; otherwise, false. - public static bool operator !=(ItemWithGroupKey left, ItemWithGroupKey right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() => $"{Item} ({Group})"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/GroupOnImmutable.cs b/src/DynamicData/List/Internal/GroupOnImmutable.cs index bd8f8d55f..1e809c43f 100644 --- a/src/DynamicData/List/Internal/GroupOnImmutable.cs +++ b/src/DynamicData/List/Internal/GroupOnImmutable.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,18 +8,21 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class GroupOnImmutable + where TGroupKey : notnull { - private readonly IObservable> _source; private readonly Func _groupSelector; - private readonly IObservable _reGrouper; - public GroupOnImmutable([NotNull] IObservable> source, [NotNull] Func groupSelector, IObservable reGrouper) + private readonly IObservable? _reGrouper; + + private readonly IObservable> _source; + + public GroupOnImmutable(IObservable> source, Func groupSelector, IObservable? reGrouper) { _source = source ?? throw new ArgumentNullException(nameof(source)); _groupSelector = groupSelector ?? throw new ArgumentNullException(nameof(groupSelector)); @@ -28,95 +31,97 @@ public GroupOnImmutable([NotNull] IObservable> source, [NotN public IObservable>> Run() { - return Observable.Create>>(observer => - { - var groupings = new ChangeAwareList>(); - var groupCache = new Dictionary(); + return Observable.Create>>( + observer => + { + var groupings = new ChangeAwareList>(); + var groupCache = new Dictionary(); - //var itemsWithGroup = _source - // .Transform(t => new ItemWithValue(t, _groupSelector(t))); + // var itemsWithGroup = _source + // .Transform(t => new ItemWithValue(t, _groupSelector(t))); - //capture the grouping up front which has the benefit that the group key is only selected once - var itemsWithGroup = _source - .Transform((t, previous) => - { - return new ItemWithGroupKey(t, _groupSelector(t), previous.Convert(p => p.Group)); - }, true); + // capture the grouping up front which has the benefit that the group key is only selected once + var itemsWithGroup = _source.Transform((t, previous) => new ItemWithGroupKey(t, _groupSelector(t), previous.Convert(p => p.Group)), true); - var locker = new object(); - var shared = itemsWithGroup.Synchronize(locker).Publish(); + var locker = new object(); + var shared = itemsWithGroup.Synchronize(locker).Publish(); - var grouper = shared - .Select(changes => Process(groupings, groupCache, changes)); + var grouper = shared.Select(changes => Process(groupings, groupCache, changes)); - IObservable>> reGrouper; - if (_reGrouper == null) - { - reGrouper = Observable.Never>>(); - } - else - { - reGrouper = _reGrouper.Synchronize(locker) - .CombineLatest(shared.ToCollection(), (_, collection) => Regroup(groupings, groupCache, collection)); - } + IObservable>> reGrouper = _reGrouper is null ? + Observable.Never>>() : + _reGrouper.Synchronize(locker).CombineLatest(shared.ToCollection(), (_, collection) => Regroup(groupings, groupCache, collection)); - var publisher = grouper.Merge(reGrouper) - .NotEmpty() - .SubscribeSafe(observer); + var publisher = grouper.Merge(reGrouper).NotEmpty().SubscribeSafe(observer); - return new CompositeDisposable(publisher, shared.Connect()); - }); + return new CompositeDisposable(publisher, shared.Connect()); + }); } - private IChangeSet> Regroup(ChangeAwareList> result, - IDictionary allGroupings, - IReadOnlyCollection currentItems) + private static IChangeSet> CreateChangeSet(ChangeAwareList> result, IDictionary allGroupings, IDictionary> initialStateOfGroups) { - var initialStateOfGroups = new Dictionary>(); - - foreach (var itemWithValue in currentItems) + // Now maintain target list + foreach (var initialGroup in initialStateOfGroups) { - var currentGroupKey = itemWithValue.Group; - var newGroupKey = _groupSelector(itemWithValue.Item); - if (newGroupKey.Equals(currentGroupKey)) + var key = initialGroup.Key; + var current = allGroupings[initialGroup.Key]; + + if (current.List.Count == 0) { - continue; + // remove if empty + allGroupings.Remove(key); + result.Remove(initialGroup.Value); } - - //lookup group and if created, add to result set - var oldGrouping = GetGroup(allGroupings, currentGroupKey); - if (!initialStateOfGroups.ContainsKey(currentGroupKey)) + else { - initialStateOfGroups[currentGroupKey] = GetGroupState(oldGrouping); + var currentState = GetGroupState(current); + if (initialGroup.Value.Count == 0) + { + // an add + result.Add(currentState); + } + else + { + // a replace (or add if the old group has already been removed) + result.Replace(initialGroup.Value, currentState); + } } + } - //remove from the old group - oldGrouping.List.Remove(itemWithValue.Item); + return result.CaptureChanges(); + } - //Mark the old item with the new cache group - itemWithValue.Group = newGroupKey; + private static GroupContainer GetGroup(IDictionary groupCaches, TGroupKey key) + { + var cached = groupCaches.Lookup(key); + if (cached.HasValue) + { + return cached.Value; + } - //add to the new group - var newGrouping = GetGroup(allGroupings, newGroupKey); - if (!initialStateOfGroups.ContainsKey(newGroupKey)) - { - initialStateOfGroups[newGroupKey] = GetGroupState(newGrouping); - } + var newcache = new GroupContainer(key); + groupCaches[key] = newcache; + return newcache; + } - newGrouping.List.Add(itemWithValue.Item); - } + private static IGrouping GetGroupState(GroupContainer grouping) + { + return new ImmutableGroup(grouping.Key, grouping.List); + } - return CreateChangeSet(result, allGroupings, initialStateOfGroups); + private static IGrouping GetGroupState(TGroupKey key, IList list) + { + return new ImmutableGroup(key, list); } private static IChangeSet> Process(ChangeAwareList> result, IDictionary allGroupings, IChangeSet changes) { - //need to keep track of effected groups to calculate correct notifications + // need to keep track of effected groups to calculate correct notifications var initialStateOfGroups = new Dictionary>(); foreach (var grouping in changes.Unified().GroupBy(change => change.Current.Group)) { - //lookup group and if created, add to result set + // lookup group and if created, add to result set var currentGroup = grouping.Key; var groupContainer = GetGroup(allGroupings, currentGroup); @@ -130,95 +135,95 @@ void GetInitialState() var listToModify = groupContainer.List; - //iterate through the group's items and process + // iterate through the group's items and process foreach (var change in grouping) { switch (change.Reason) { case ListChangeReason.Add: - { - GetInitialState(); - listToModify.Add(change.Current.Item); - break; - } - - case ListChangeReason.Refresh: - { - var previousItem = change.Current.Item; - var previousGroup = change.Current.PreviousGroup.Value; - var currentItem = change.Current.Item; - - //check whether an item changing has resulted in a different group - if (!previousGroup.Equals(currentGroup)) { - GetInitialState(); - //add to new group - listToModify.Add(currentItem); - - //remove from old group - allGroupings.Lookup(previousGroup) - .IfHasValue(g => - { - if (!initialStateOfGroups.ContainsKey(g.Key)) - { - initialStateOfGroups[g.Key] = GetGroupState(g.Key, g.List); - } - - g.List.Remove(previousItem); - }); + listToModify.Add(change.Current.Item); + break; } - break; - } - - case ListChangeReason.Replace: - { - GetInitialState(); - var previousItem = change.Previous.Value.Item; - var previousGroup = change.Previous.Value.Group; - - //check whether an item changing has resulted in a different group - if (previousGroup.Equals(currentGroup)) + case ListChangeReason.Refresh: { - //find and replace - var index = listToModify.IndexOf(previousItem); - listToModify[index] = change.Current.Item; + var previousItem = change.Current.Item; + var previousGroup = change.Current.PreviousGroup.Value; + var currentItem = change.Current.Item; + + // check whether an item changing has resulted in a different group + if (previousGroup.Equals(currentGroup) == false) + { + GetInitialState(); + + // add to new group + listToModify.Add(currentItem); + + // remove from old group + allGroupings.Lookup(previousGroup).IfHasValue( + g => + { + if (!initialStateOfGroups.ContainsKey(g.Key)) + { + initialStateOfGroups[g.Key] = GetGroupState(g.Key, g.List); + } + + g.List.Remove(previousItem); + }); + } + + break; } - else - { - //add to new group - listToModify.Add(change.Current.Item); - //remove from old group - allGroupings.Lookup(previousGroup) - .IfHasValue(g => - { - if (!initialStateOfGroups.ContainsKey(g.Key)) - { - initialStateOfGroups[g.Key] = GetGroupState(g.Key, g.List); - } - - g.List.Remove(previousItem); - }); + case ListChangeReason.Replace: + { + GetInitialState(); + var previousItem = change.Previous.Value.Item; + var previousGroup = change.Previous.Value.Group; + + // check whether an item changing has resulted in a different group + if (previousGroup.Equals(currentGroup)) + { + // find and replace + var index = listToModify.IndexOf(previousItem); + listToModify[index] = change.Current.Item; + } + else + { + // add to new group + listToModify.Add(change.Current.Item); + + // remove from old group + allGroupings.Lookup(previousGroup).IfHasValue( + g => + { + if (!initialStateOfGroups.ContainsKey(g.Key)) + { + initialStateOfGroups[g.Key] = GetGroupState(g.Key, g.List); + } + + g.List.Remove(previousItem); + }); + } + + break; } - break; - } - case ListChangeReason.Remove: - { - GetInitialState(); - listToModify.Remove(change.Current.Item); - break; - } + { + GetInitialState(); + listToModify.Remove(change.Current.Item); + break; + } case ListChangeReason.Clear: - { - GetInitialState(); - listToModify.Clear(); - break; - } + { + GetInitialState(); + listToModify.Clear(); + break; + } } } } @@ -226,79 +231,59 @@ void GetInitialState() return CreateChangeSet(result, allGroupings, initialStateOfGroups); } - private static IChangeSet> CreateChangeSet(ChangeAwareList> result, IDictionary allGroupings, IDictionary> initialStateOfGroups) + private IChangeSet> Regroup(ChangeAwareList> result, IDictionary allGroupings, IReadOnlyCollection currentItems) { - //Now maintain target list - foreach (var intialGroup in initialStateOfGroups) - { - var key = intialGroup.Key; - var current = allGroupings[intialGroup.Key]; + var initialStateOfGroups = new Dictionary>(); - if (current.List.Count == 0) + foreach (var itemWithValue in currentItems) + { + var currentGroupKey = itemWithValue.Group; + var newGroupKey = _groupSelector(itemWithValue.Item); + if (newGroupKey.Equals(currentGroupKey)) { - //remove if empty - allGroupings.Remove(key); - result.Remove(intialGroup.Value); + continue; } - else + + // lookup group and if created, add to result set + var oldGrouping = GetGroup(allGroupings, currentGroupKey); + if (!initialStateOfGroups.ContainsKey(currentGroupKey)) { - var currentState = GetGroupState(current); - if (intialGroup.Value.Count == 0) - { - //an add - result.Add(currentState); - } - else - { - //a replace (or add if the old group has already been removed) - result.Replace(intialGroup.Value, currentState); - } + initialStateOfGroups[currentGroupKey] = GetGroupState(oldGrouping); } - } - return result.CaptureChanges(); - } + // remove from the old group + oldGrouping.List.Remove(itemWithValue.Item); - private static IGrouping GetGroupState(GroupContainer grouping) - { - return new ImmutableGroup(grouping.Key, grouping.List); - } + // Mark the old item with the new cache group + itemWithValue.Group = newGroupKey; - private static IGrouping GetGroupState(TGroupKey key, IList list) - { - return new ImmutableGroup(key, list); - } + // add to the new group + var newGrouping = GetGroup(allGroupings, newGroupKey); + if (!initialStateOfGroups.ContainsKey(newGroupKey)) + { + initialStateOfGroups[newGroupKey] = GetGroupState(newGrouping); + } - private static GroupContainer GetGroup(IDictionary groupCaches, TGroupKey key) - { - var cached = groupCaches.Lookup(key); - if (cached.HasValue) - { - return cached.Value; + newGrouping.List.Add(itemWithValue.Item); } - var newcache = new GroupContainer(key); - groupCaches[key] = newcache; - return newcache; + return CreateChangeSet(result, allGroupings, initialStateOfGroups); } private class GroupContainer { - public IList List { get; } = new List(); - public TGroupKey Key { get; } - public GroupContainer(TGroupKey key) { Key = key; } + + public TGroupKey Key { get; } + + public IList List { get; } = new List(); } private sealed class ItemWithGroupKey : IEquatable { - public TObject Item { get; } - public TGroupKey Group { get; set; } - public Optional PreviousGroup { get; } - public ItemWithGroupKey(TObject item, TGroupKey group, Optional previousGroup) { Item = item; @@ -306,55 +291,47 @@ public ItemWithGroupKey(TObject item, TGroupKey group, Optional previ PreviousGroup = previousGroup; } - #region Equality + public TGroupKey Group { get; set; } - public bool Equals(ItemWithGroupKey other) - { - if (other is null) - { - return false; - } + public TObject Item { get; } - if (ReferenceEquals(this, other)) - { - return true; - } + public Optional PreviousGroup { get; } - return EqualityComparer.Default.Equals(Item, other.Item); + public static bool operator ==(ItemWithGroupKey left, ItemWithGroupKey right) + { + return Equals(left, right); } - public override bool Equals(object obj) + public static bool operator !=(ItemWithGroupKey left, ItemWithGroupKey right) { - if (obj is null) + return !Equals(left, right); + } + + public bool Equals(ItemWithGroupKey? other) + { + if (other is null) { return false; } - if (ReferenceEquals(this, obj)) + if (ReferenceEquals(this, other)) { return true; } - return obj is ItemWithGroupKey && Equals((ItemWithGroupKey)obj); + return EqualityComparer.Default.Equals(Item, other.Item); } - public override int GetHashCode() + public override bool Equals(object? obj) { - return EqualityComparer.Default.GetHashCode(Item); + return obj is ItemWithGroupKey value && Equals(value); } - public static bool operator ==(ItemWithGroupKey left, ItemWithGroupKey right) - { - return Equals(left, right); - } - - public static bool operator !=(ItemWithGroupKey left, ItemWithGroupKey right) + public override int GetHashCode() { - return !Equals(left, right); + return Item is null ? 0 : EqualityComparer.Default.GetHashCode(Item); } - #endregion - public override string ToString() { return $"{Item} ({Group})"; diff --git a/src/DynamicData/List/Internal/GroupOnProperty.cs b/src/DynamicData/List/Internal/GroupOnProperty.cs index b38ff1ef9..3a185074d 100644 --- a/src/DynamicData/List/Internal/GroupOnProperty.cs +++ b/src/DynamicData/List/Internal/GroupOnProperty.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,23 +7,29 @@ using System.Linq.Expressions; using System.Reactive.Concurrency; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.List.Internal { - internal sealed class GroupOnProperty + internal sealed class GroupOnProperty + where TGroup : notnull where TObject : INotifyPropertyChanged { - private readonly IObservable> _source; + private readonly Func _groupSelector; + private readonly Expression> _propertySelector; + + private readonly IScheduler? _scheduler; + + private readonly IObservable> _source; + private readonly TimeSpan? _throttle; - private readonly IScheduler _scheduler; - private readonly Func _groupSelector; - public GroupOnProperty(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler scheduler = null) + public GroupOnProperty(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); - _groupSelector = groupSelectorKey?.Compile() ?? throw new ArgumentNullException(nameof(groupSelectorKey)); + _groupSelector = groupSelectorKey.Compile(); _propertySelector = groupSelectorKey; _throttle = throttle; _scheduler = scheduler; @@ -31,20 +37,21 @@ public GroupOnProperty(IObservable> source, Expression>> Run() { - return _source.Publish(shared => - { - // Monitor explicit property changes - var regrouper = shared.WhenValueChanged(_propertySelector, false).ToUnit(); - - //add a throttle if specified - if (_throttle != null) - { - regrouper = regrouper.Throttle(_throttle.Value, _scheduler ?? Scheduler.Default); - } - - // Use property changes as a trigger to re-evaluate Grouping - return shared.GroupOn(_groupSelector, regrouper); - }); + return _source.Publish( + shared => + { + // Monitor explicit property changes + var regrouper = shared.WhenValueChanged(_propertySelector, false).ToUnit(); + + // add a throttle if specified + if (_throttle is not null) + { + regrouper = regrouper.Throttle(_throttle.Value, _scheduler ?? Scheduler.Default); + } + + // Use property changes as a trigger to re-evaluate Grouping + return shared.GroupOn(_groupSelector, regrouper); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/GroupOnPropertyWithImmutableState.cs b/src/DynamicData/List/Internal/GroupOnPropertyWithImmutableState.cs index 062e60f4c..4389aa87d 100644 --- a/src/DynamicData/List/Internal/GroupOnPropertyWithImmutableState.cs +++ b/src/DynamicData/List/Internal/GroupOnPropertyWithImmutableState.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,23 +7,29 @@ using System.Linq.Expressions; using System.Reactive.Concurrency; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class GroupOnPropertyWithImmutableState + where TGroup : notnull where TObject : INotifyPropertyChanged { - private readonly IObservable> _source; + private readonly Func _groupSelector; + private readonly Expression> _propertySelector; + + private readonly IScheduler? _scheduler; + + private readonly IObservable> _source; + private readonly TimeSpan? _throttle; - private readonly IScheduler _scheduler; - private readonly Func _groupSelector; - public GroupOnPropertyWithImmutableState(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler scheduler = null) + public GroupOnPropertyWithImmutableState(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler? scheduler = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); - _groupSelector = groupSelectorKey?.Compile() ?? throw new ArgumentNullException(nameof(groupSelectorKey)); + _groupSelector = groupSelectorKey.Compile(); _propertySelector = groupSelectorKey; _throttle = throttle; _scheduler = scheduler; @@ -31,20 +37,21 @@ public GroupOnPropertyWithImmutableState(IObservable> source public IObservable>> Run() { - return _source.Publish(shared => - { - // Monitor explicit property changes - var regrouper = shared.WhenValueChanged(_propertySelector, false).ToUnit(); - - //add a throttle if specified - if (_throttle != null) - { - regrouper = regrouper.Throttle(_throttle.Value, _scheduler ?? Scheduler.Default); - } - - // Use property changes as a trigger to re-evaluate Grouping - return shared.GroupWithImmutableState(_groupSelector, regrouper); - }); + return _source.Publish( + shared => + { + // Monitor explicit property changes + var regrouper = shared.WhenValueChanged(_propertySelector, false).ToUnit(); + + // add a throttle if specified + if (_throttle is not null) + { + regrouper = regrouper.Throttle(_throttle.Value, _scheduler ?? Scheduler.Default); + } + + // Use property changes as a trigger to re-evaluate Grouping + return shared.GroupWithImmutableState(_groupSelector, regrouper); + }); } } } \ No newline at end of file diff --git a/src/DynamicData/List/Internal/ImmutableGroup.cs b/src/DynamicData/List/Internal/ImmutableGroup.cs index ff50f949f..99f9563d4 100644 --- a/src/DynamicData/List/Internal/ImmutableGroup.cs +++ b/src/DynamicData/List/Internal/ImmutableGroup.cs @@ -1,19 +1,18 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; namespace DynamicData.List.Internal { - internal sealed class ImmutableGroup : IGrouping, IEquatable> + internal sealed class ImmutableGroup : IGrouping, IEquatable> { private readonly IReadOnlyCollection _items; - public TGroupKey Key { get; } - internal ImmutableGroup(TGroupKey key, IList items) { Key = key; @@ -21,11 +20,22 @@ internal ImmutableGroup(TGroupKey key, IList items) } public int Count => _items.Count; + public IEnumerable Items => _items; - #region Equality + public TGroupKey Key { get; } + + public static bool operator ==(ImmutableGroup left, ImmutableGroup right) + { + return Equals(left, right); + } + + public static bool operator !=(ImmutableGroup left, ImmutableGroup right) + { + return !Equals(left, right); + } - public bool Equals(ImmutableGroup other) + public bool Equals(ImmutableGroup? other) { if (ReferenceEquals(null, other)) { @@ -40,7 +50,7 @@ public bool Equals(ImmutableGroup other) return EqualityComparer.Default.Equals(Key, other.Key); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -52,26 +62,14 @@ public override bool Equals(object obj) return true; } - return obj is ImmutableGroup && Equals((ImmutableGroup) obj); + return obj is ImmutableGroup value && Equals(value); } public override int GetHashCode() { - return EqualityComparer.Default.GetHashCode(Key); - } - - public static bool operator ==(ImmutableGroup left, ImmutableGroup right) - { - return Equals(left, right); + return Key is null ? 0 : EqualityComparer.Default.GetHashCode(Key); } - public static bool operator !=(ImmutableGroup left, ImmutableGroup right) - { - return !Equals(left, right); - } - - #endregion - public override string ToString() { return $"Grouping for: {Key} ({Count} items)"; diff --git a/src/DynamicData/List/Internal/LimitSizeTo.cs b/src/DynamicData/List/Internal/LimitSizeTo.cs new file mode 100644 index 000000000..901b3eb99 --- /dev/null +++ b/src/DynamicData/List/Internal/LimitSizeTo.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2011-2020 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Threading; + +namespace DynamicData.List.Internal +{ + internal sealed class LimitSizeTo + { + private readonly object _locker; + + private readonly IScheduler _scheduler; + + private readonly int _sizeLimit; + + private readonly ISourceList _sourceList; + + public LimitSizeTo(ISourceList sourceList, int sizeLimit, IScheduler scheduler, object locker) + { + _sourceList = sourceList ?? throw new ArgumentNullException(nameof(sourceList)); + _sizeLimit = sizeLimit; + _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler)); + _locker = locker; + } + + public IObservable> Run() + { + var emptyResult = new List(); + long orderItemWasAdded = -1; + + return _sourceList.Connect().ObserveOn(_scheduler).Synchronize(_locker).Transform(t => new ExpirableItem(t, _scheduler.Now.DateTime, Interlocked.Increment(ref orderItemWasAdded))).ToCollection().Select( + list => + { + var numberToExpire = list.Count - _sizeLimit; + if (numberToExpire < 0) + { + return emptyResult; + } + + return list.OrderBy(exp => exp.ExpireAt).ThenBy(exp => exp.Index).Take(numberToExpire).Select(item => item.Item).ToList(); + }).Where(items => items.Count != 0); + } + } +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/MergeMany.cs b/src/DynamicData/List/Internal/MergeMany.cs index 45da5ee0d..0c00dc154 100644 --- a/src/DynamicData/List/Internal/MergeMany.cs +++ b/src/DynamicData/List/Internal/MergeMany.cs @@ -1,20 +1,19 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.List.Internal { internal sealed class MergeMany { - private readonly IObservable> _source; private readonly Func> _observableSelector; - public MergeMany([NotNull] IObservable> source, - [NotNull] Func> observableSelector) + private readonly IObservable> _source; + + public MergeMany(IObservable> source, Func> observableSelector) { _source = source ?? throw new ArgumentNullException(nameof(source)); _observableSelector = observableSelector ?? throw new ArgumentNullException(nameof(observableSelector)); @@ -22,15 +21,12 @@ public MergeMany([NotNull] IObservable> source, public IObservable Run() { - return Observable.Create - ( - observer => + return Observable.Create( + observer => { var locker = new object(); - return _source - .SubscribeMany(t => _observableSelector(t).Synchronize(locker).Subscribe(observer.OnNext)) - .Subscribe(t => { }, observer.OnError); + return _source.SubscribeMany(t => _observableSelector(t).Synchronize(locker).Subscribe(observer.OnNext)).Subscribe(_ => { }, observer.OnError); }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/OnBeingAdded.cs b/src/DynamicData/List/Internal/OnBeingAdded.cs index 52eb84709..50595f226 100644 --- a/src/DynamicData/List/Internal/OnBeingAdded.cs +++ b/src/DynamicData/List/Internal/OnBeingAdded.cs @@ -1,18 +1,20 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Linq; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class OnBeingAdded { - private readonly IObservable> _source; private readonly Action _callback; + private readonly IObservable> _source; + public OnBeingAdded(IObservable> source, Action callback) { _source = source ?? throw new ArgumentNullException(nameof(source)); @@ -33,9 +35,11 @@ private void RegisterForAddition(IChangeSet changes) case ListChangeReason.Add: _callback(change.Item.Current); break; + case ListChangeReason.AddRange: change.Range.ForEach(_callback); break; + case ListChangeReason.Replace: _callback(change.Item.Current); break; @@ -43,4 +47,4 @@ private void RegisterForAddition(IChangeSet changes) } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/OnBeingRemoved.cs b/src/DynamicData/List/Internal/OnBeingRemoved.cs index b310f9823..f24be00c8 100644 --- a/src/DynamicData/List/Internal/OnBeingRemoved.cs +++ b/src/DynamicData/List/Internal/OnBeingRemoved.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,17 +6,18 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class OnBeingRemoved { - private readonly IObservable> _source; private readonly Action _callback; - public OnBeingRemoved([NotNull] IObservable> source, [NotNull] Action callback) + private readonly IObservable> _source; + + public OnBeingRemoved(IObservable> source, Action callback) { _source = source ?? throw new ArgumentNullException(nameof(source)); _callback = callback ?? throw new ArgumentNullException(nameof(callback)); @@ -24,20 +25,19 @@ public OnBeingRemoved([NotNull] IObservable> source, [NotNull] Act public IObservable> Run() { - return Observable.Create>(observer => + return Observable.Create>( + observer => { var locker = new object(); var items = new List(); - var subscriber = _source - .Synchronize(locker) - .Do(changes => RegisterForRemoval(items, changes), observer.OnError) - .SubscribeSafe(observer); + var subscriber = _source.Synchronize(locker).Do(changes => RegisterForRemoval(items, changes), observer.OnError).SubscribeSafe(observer); - return Disposable.Create(() => - { - subscriber.Dispose(); - items.ForEach(t => _callback(t)); - }); + return Disposable.Create( + () => + { + subscriber.Dispose(); + items.ForEach(t => _callback(t)); + }); }); } @@ -50,12 +50,15 @@ private void RegisterForRemoval(IList items, IChangeSet changes) case ListChangeReason.Replace: change.Item.Previous.IfHasValue(t => _callback(t)); break; + case ListChangeReason.Remove: _callback(change.Item.Current); break; + case ListChangeReason.RemoveRange: change.Range.ForEach(_callback); break; + case ListChangeReason.Clear: items.ForEach(_callback); break; @@ -65,4 +68,4 @@ private void RegisterForRemoval(IList items, IChangeSet changes) items.Clone(changes); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/Pager.cs b/src/DynamicData/List/Internal/Pager.cs index 88b88e245..311416e8a 100644 --- a/src/DynamicData/List/Internal/Pager.cs +++ b/src/DynamicData/List/Internal/Pager.cs @@ -1,22 +1,24 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal class Pager { - private readonly IObservable> _source; private readonly IObservable _requests; - public Pager([NotNull] IObservable> source, [NotNull] IObservable requests) + private readonly IObservable> _source; + + public Pager(IObservable> source, IObservable requests) { _source = source ?? throw new ArgumentNullException(nameof(source)); _requests = requests ?? throw new ArgumentNullException(nameof(requests)); @@ -24,35 +26,55 @@ public Pager([NotNull] IObservable> source, [NotNull] IObservable< public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); - var all = new List(); - var paged = new ChangeAwareList(); - - IPageRequest parameters = new PageRequest(0, 25); - - var requestStream = _requests - .Synchronize(locker) - .Select(request => + return Observable.Create>( + observer => { - parameters = request; - return CheckParametersAndPage(all, paged, request); + var locker = new object(); + var all = new List(); + var paged = new ChangeAwareList(); + + IPageRequest parameters = new PageRequest(0, 25); + + var requestStream = _requests.Synchronize(locker).Select( + request => + { + parameters = request; + return CheckParametersAndPage(all, paged, request); + }); + + var dataChanged = _source + .Synchronize(locker) + .Select(changes => Page(all, paged, parameters, changes)); + + return requestStream + .Merge(dataChanged) + .Where(changes => changes is not null && changes.Count != 0) + .Select(x => x!) + .SubscribeSafe(observer); }); + } - var dataChanged = _source - .Synchronize(locker) - .Select(changes => Page(all, paged, parameters, changes)); + private static int CalculatePages(ICollection all, IPageRequest? request) + { + if (request is null || request.Size >= all.Count || request.Size == 0) + { + return 1; + } - return requestStream.Merge(dataChanged) - .Where(changes => changes != null && changes.Count != 0) - .SubscribeSafe(observer); - }); + int pages = all.Count / request.Size; + int overlap = all.Count % request.Size; + + if (overlap == 0) + { + return pages; + } + + return pages + 1; } - private static PageChangeSet CheckParametersAndPage(List all, ChangeAwareList paged, IPageRequest request) + private static PageChangeSet? CheckParametersAndPage(List all, ChangeAwareList paged, IPageRequest? request) { - if (request == null || request.Page < 0 || request.Size < 1) + if (request is null || request.Page < 0 || request.Size < 1) { return null; } @@ -60,11 +82,11 @@ private static PageChangeSet CheckParametersAndPage(List all, ChangeAwareL return Page(all, paged, request); } - private static PageChangeSet Page(List all, ChangeAwareList paged, IPageRequest request, IChangeSet changeset = null) + private static PageChangeSet Page(List all, ChangeAwareList paged, IPageRequest request, IChangeSet? changeSet = null) { - if (changeset != null) + if (changeSet is not null) { - all.Clone(changeset); + all.Clone(changeSet); } var previous = paged; @@ -90,19 +112,19 @@ private static PageChangeSet Page(List all, ChangeAwareList paged, IPag var startIndex = skip; - var moves = changeset.EmptyIfNull() + var moves = changeSet.EmptyIfNull() .Where(change => change.Reason == ListChangeReason.Moved && change.MovedWithinRange(startIndex, startIndex + request.Size)); foreach (var change in moves) { - //check whether an item has moved within the same page + // check whether an item has moved within the same page var currentIndex = change.Item.CurrentIndex - startIndex; var previousIndex = change.Item.PreviousIndex - startIndex; paged.Move(previousIndex, currentIndex); } - //find replaces [Is this ever the case that it can be reached] + // find replaces [Is this ever the case that it can be reached] for (int i = 0; i < current.Count; i++) { var currentItem = current[i]; @@ -121,23 +143,5 @@ private static PageChangeSet Page(List all, ChangeAwareList paged, IPag return new PageChangeSet(changed, new PageResponse(paged.Count, page, all.Count, pages)); } - - private static int CalculatePages(List all, IPageRequest request) - { - if (request.Size >= all.Count || request.Size==0) - { - return 1; - } - - int pages = all.Count / request.Size; - int overlap = all.Count % request.Size; - - if (overlap == 0) - { - return pages; - } - - return pages + 1; - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/QueryWhenChanged.cs b/src/DynamicData/List/Internal/QueryWhenChanged.cs index 3dc2ca645..1c4248220 100644 --- a/src/DynamicData/List/Internal/QueryWhenChanged.cs +++ b/src/DynamicData/List/Internal/QueryWhenChanged.cs @@ -1,11 +1,11 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal @@ -14,19 +14,20 @@ internal class QueryWhenChanged { private readonly IObservable> _source; - public QueryWhenChanged([NotNull] IObservable> source) + public QueryWhenChanged(IObservable> source) { _source = source ?? throw new ArgumentNullException(nameof(source)); } public IObservable> Run() { - return _source.Scan(new List(), (list, changes) => - { - list.Clone(changes); - return list; - } - ).Select(list => new ReadOnlyCollectionLight(list)); + return _source.Scan( + new List(), + (list, changes) => + { + list.Clone(changes); + return list; + }).Select(list => new ReadOnlyCollectionLight(list)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/ReaderWriter.cs b/src/DynamicData/List/Internal/ReaderWriter.cs index e9c2d82b1..8c486274f 100644 --- a/src/DynamicData/List/Internal/ReaderWriter.cs +++ b/src/DynamicData/List/Internal/ReaderWriter.cs @@ -1,87 +1,79 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class ReaderWriter { - private ChangeAwareList _data = new ChangeAwareList(); - private readonly object _locker = new object(); + private readonly object _locker = new(); + + private ChangeAwareList _data = new(); + private bool _updateInProgress; - public IChangeSet Write(IChangeSet changes) + public int Count { - if (changes == null) + get { - throw new ArgumentNullException(nameof(changes)); + lock (_locker) + { + return _data.Count; + } } + } - IChangeSet result; - - lock (_locker) + public T[] Items + { + get { - _data.Clone(changes); - result = _data.CaptureChanges(); + lock (_locker) + { + var result = new T[_data.Count]; + _data.CopyTo(result, 0); + return result; + } } - - return result; } - public IChangeSet Write(Action> updateAction) + public IChangeSet Write(IChangeSet changes) { - if (updateAction == null) + if (changes is null) { - throw new ArgumentNullException(nameof(updateAction)); + throw new ArgumentNullException(nameof(changes)); } IChangeSet result; - // Write straight to the list, no preview lock (_locker) { - _updateInProgress = true; - updateAction(_data); - _updateInProgress = false; + _data.Clone(changes); result = _data.CaptureChanges(); } return result; } - public IChangeSet WriteWithPreview(Action> updateAction, Action> previewHandler) + public IChangeSet Write(Action> updateAction) { - if (updateAction == null) + if (updateAction is null) { throw new ArgumentNullException(nameof(updateAction)); } - if (previewHandler == null) - { - throw new ArgumentNullException(nameof(previewHandler)); - } - IChangeSet result; - // Make a copy, apply changes on the main list, perform the preview callback with the old list and swap the lists again to finalize the update. + // Write straight to the list, no preview lock (_locker) { - ChangeAwareList copy = new ChangeAwareList(_data, false); - _updateInProgress = true; updateAction(_data); _updateInProgress = false; - result = _data.CaptureChanges(); - - InternalEx.Swap(ref _data, ref copy); - - previewHandler(result); - - InternalEx.Swap(ref _data, ref copy); } return result; @@ -92,9 +84,10 @@ public IChangeSet WriteWithPreview(Action> updateAction, Act /// Changes are added to the topmost change tracker. /// Use only during an invocation of Write/WriteWithPreview. /// + /// The action to perform on the list. public void WriteNested(Action> updateAction) { - if (updateAction == null) + if (updateAction is null) { throw new ArgumentNullException(nameof(updateAction)); } @@ -110,28 +103,39 @@ public void WriteNested(Action> updateAction) } } - public T[] Items + public IChangeSet WriteWithPreview(Action> updateAction, Action> previewHandler) { - get + if (updateAction is null) { - lock (_locker) - { - var result = new T[_data.Count]; - _data.CopyTo(result, 0); - return result; - } + throw new ArgumentNullException(nameof(updateAction)); } - } - public int Count - { - get + if (previewHandler is null) { - lock (_locker) - { - return _data.Count; - } + throw new ArgumentNullException(nameof(previewHandler)); } + + IChangeSet result; + + // Make a copy, apply changes on the main list, perform the preview callback with the old list and swap the lists again to finalize the update. + lock (_locker) + { + ChangeAwareList copy = new(_data, false); + + _updateInProgress = true; + updateAction(_data); + _updateInProgress = false; + + result = _data.CaptureChanges(); + + InternalEx.Swap(ref _data, ref copy); + + previewHandler(result); + + InternalEx.Swap(ref _data, ref copy); + } + + return result; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/RefCount.cs b/src/DynamicData/List/Internal/RefCount.cs index aae2f459e..56f2ae5b3 100644 --- a/src/DynamicData/List/Internal/RefCount.cs +++ b/src/DynamicData/List/Internal/RefCount.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -10,10 +10,13 @@ namespace DynamicData.List.Internal { internal class RefCount { + private readonly object _locker = new(); + private readonly IObservable> _source; - private readonly object _locker = new object(); + + private IObservableList? _list; + private int _refCount; - private IObservableList _list; public RefCount(IObservable> source) { @@ -22,34 +25,41 @@ public RefCount(IObservable> source) public IObservable> Run() { - return Observable.Create>(observer => - { - lock (_locker) - { - if (++_refCount == 1) + return Observable.Create>( + observer => { - _list = _source.AsObservableList(); - } - } - - var subscriber = _list.Connect().SubscribeSafe(observer); + lock (_locker) + { + if (++_refCount == 1) + { + _list = _source.AsObservableList(); + } + } - return Disposable.Create(() => - { - subscriber.Dispose(); - IDisposable listToDispose = null; - lock (_locker) - { - if (--_refCount == 0) + if (_list is null) { - listToDispose = _list; - _list = null; + throw new InvalidOperationException("The list is null despite having reference counting."); } - } - listToDispose?.Dispose(); - }); - }); + var subscriber = _list.Connect().SubscribeSafe(observer); + + return Disposable.Create( + () => + { + subscriber.Dispose(); + IDisposable? listToDispose = null; + lock (_locker) + { + if (--_refCount == 0) + { + listToDispose = _list; + _list = null; + } + } + + listToDispose?.Dispose(); + }); + }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/ReferenceCountTracker.cs b/src/DynamicData/List/Internal/ReferenceCountTracker.cs index 3110b1e53..a36d602df 100644 --- a/src/DynamicData/List/Internal/ReferenceCountTracker.cs +++ b/src/DynamicData/List/Internal/ReferenceCountTracker.cs @@ -1,7 +1,8 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. +using System; using System.Collections.Generic; namespace DynamicData.List.Internal @@ -9,22 +10,30 @@ namespace DynamicData.List.Internal /// /// Ripped and adapted from https://clinq.codeplex.com/ /// - /// Thanks dudes + /// Thanks dudes. /// - /// + /// The type of the item. internal class ReferenceCountTracker { - private Dictionary ReferenceCounts { get; } = new Dictionary(); - public IEnumerable Items => ReferenceCounts.Keys; +#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. + private Dictionary ReferenceCounts { get; } = new(); +#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. + public int this[T item] => ReferenceCounts[item]; /// - /// Increments the reference count for the item. Returns true when refrence count goes from 0 to 1. + /// Increments the reference count for the item. Returns true when reference count goes from 0 to 1. /// + /// The item to add. public bool Add(T item) { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + if (!ReferenceCounts.TryGetValue(item, out var currentCount)) { ReferenceCounts.Add(item, 1); @@ -40,11 +49,22 @@ public void Clear() ReferenceCounts.Clear(); } + public bool Contains(T item) + { + return ReferenceCounts.ContainsKey(item); + } + /// - /// Decrements the reference count for the item. Returns true when refrence count goes from 1 to 0. + /// Decrements the reference count for the item. Returns true when reference count goes from 1 to 0. /// + /// The item to remove. public bool Remove(T item) { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + int currentCount = ReferenceCounts[item]; if (currentCount == 1) @@ -56,10 +76,5 @@ public bool Remove(T item) ReferenceCounts[item] = currentCount - 1; return false; } - - public bool Contains(T item) - { - return ReferenceCounts.ContainsKey(item); - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/SizeLimiter.cs b/src/DynamicData/List/Internal/SizeLimiter.cs deleted file mode 100644 index 7c5ff37bc..000000000 --- a/src/DynamicData/List/Internal/SizeLimiter.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2011-2019 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. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Concurrency; -using System.Reactive.Linq; -using System.Threading; -using DynamicData.Annotations; - -namespace DynamicData.List.Internal -{ - internal sealed class LimitSizeTo - { - private readonly ISourceList _sourceList; - private readonly IScheduler _scheduler; - private readonly object _locker; - private readonly int _sizeLimit; - - public LimitSizeTo([NotNull] ISourceList sourceList, int sizeLimit, [NotNull] IScheduler scheduler, object locker) - { - _sourceList = sourceList ?? throw new ArgumentNullException(nameof(sourceList)); - _sizeLimit = sizeLimit; - _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler)); - _locker = locker; - } - - public IObservable> Run() - { - var emptyResult = new List(); - long orderItemWasAdded = -1; - - return _sourceList.Connect() - .ObserveOn(_scheduler) - .Synchronize(_locker) - .Transform(t => new ExpirableItem(t, _scheduler.Now.DateTime, Interlocked.Increment(ref orderItemWasAdded))) - .ToCollection() - .Select(list => - { - var numbertoExpire = list.Count - _sizeLimit; - if (numbertoExpire < 0) - { - return emptyResult; - } - - var dueForExpiry = list.OrderBy(exp => exp.ExpireAt).ThenBy(exp => exp.Index) - .Take(numbertoExpire) - .Select(item => item.Item) - .ToList(); - return dueForExpiry; - }).Where(items => items.Count != 0); - } - } -} diff --git a/src/DynamicData/List/Internal/Sort.cs b/src/DynamicData/List/Internal/Sort.cs index 37060150c..c0fcb1a96 100644 --- a/src/DynamicData/List/Internal/Sort.cs +++ b/src/DynamicData/List/Internal/Sort.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,64 +7,128 @@ using System.Linq; using System.Reactive; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class Sort { - private readonly IObservable> _source; - private readonly SortOptions _sortOptions; + private readonly IObservable> _comparerObservable; + private readonly int _resetThreshold; + private readonly IObservable _resort; - private readonly IObservable> _comparerObservable; + + private readonly SortOptions _sortOptions; + + private readonly IObservable> _source; private IComparer _comparer; - public Sort([NotNull] IObservable> source, - [NotNull] IComparer comparer, SortOptions sortOptions, - IObservable resort, - IObservable> comparerObservable, - int resetThreshold) + public Sort(IObservable> source, IComparer? comparer, SortOptions sortOptions, IObservable? resort, IObservable>? comparerObservable, int resetThreshold) { _source = source ?? throw new ArgumentNullException(nameof(source)); _resort = resort ?? Observable.Never(); _comparerObservable = comparerObservable ?? Observable.Never>(); - _comparer = comparer; + _comparer = comparer ?? Comparer.Default; _sortOptions = sortOptions; _resetThreshold = resetThreshold; } public IObservable> Run() { - return Observable.Create>(observer => + return Observable.Create>( + observer => + { + var locker = new object(); + var original = new List(); + var target = new ChangeAwareList(); + + var changed = _source.Synchronize(locker).Select( + changes => + { + if (_resetThreshold > 1) + { + original.Clone(changes); + } + + return changes.TotalChanges > _resetThreshold ? Reset(original, target) : Process(target, changes); + }); + var resort = _resort.Synchronize(locker).Select(_ => Reorder(target)); + var changeComparer = _comparerObservable.Synchronize(locker).Select(comparer => ChangeComparer(target, comparer)); + + return changed.Merge(resort).Merge(changeComparer).Where(changes => changes.Count != 0).SubscribeSafe(observer); + }); + } + + private IChangeSet ChangeComparer(ChangeAwareList target, IComparer comparer) + { + _comparer = comparer; + if (_resetThreshold > 0 && target.Count <= _resetThreshold) + { + return Reorder(target); + } + + var sorted = target.OrderBy(t => t, _comparer).ToList(); + target.Clear(); + target.AddRange(sorted); + return target.CaptureChanges(); + } + + private int GetCurrentPosition(ChangeAwareList target, T item) + { + var index = _sortOptions == SortOptions.UseBinarySearch ? target.BinarySearch(item, _comparer) : target.IndexOf(item); + + if (index < 0) + { + throw new SortException($"Cannot find item: {typeof(T).Name} -> {item}"); + } + + return index; + } + + private int GetInsertPosition(ChangeAwareList target, T item) + { + return _sortOptions == SortOptions.UseBinarySearch ? GetInsertPositionBinary(target, item) : GetInsertPositionLinear(target, item); + } + + private int GetInsertPositionBinary(ChangeAwareList target, T item) + { + int index = target.BinarySearch(item, _comparer); + int insertIndex = ~index; + + // sort is not returning uniqueness + if (insertIndex < 0) { - var locker = new object(); - var orginal = new List(); - var target = new ChangeAwareList(); + throw new SortException("Binary search has been specified, yet the sort does not yield uniqueness"); + } - var changed = _source.Synchronize(locker).Select(changes => + return insertIndex; + } + + private int GetInsertPositionLinear(ChangeAwareList target, T item) + { + for (var i = 0; i < target.Count; i++) + { + if (_comparer.Compare(item, target[i]) < 0) { - if (_resetThreshold > 1) - { - orginal.Clone(changes); - } - - return changes.TotalChanges > _resetThreshold && _comparer!=null ? Reset(orginal, target) : Process(target, changes); - }); - var resort = _resort.Synchronize(locker).Select(changes => Reorder(target)); - var changeComparer = _comparerObservable.Synchronize(locker).Select(comparer => ChangeComparer(target, comparer)); - - return changed.Merge(resort).Merge(changeComparer) - .Where(changes => changes.Count != 0) - .SubscribeSafe(observer); - }); + return i; + } + } + + return target.Count; + } + + private void Insert(ChangeAwareList target, T item) + { + var index = GetInsertPosition(target, item); + target.Insert(index, item); } private IChangeSet Process(ChangeAwareList target, IChangeSet changes) { - //if all removes and not Clear, then more efficient to try clear range + // if all removes and not Clear, then more efficient to try clear range if (changes.TotalChanges == changes.Removes && changes.All(c => c.Reason != ListChangeReason.Clear) && changes.Removes > 1) { var removed = changes.Unified().Select(u => u.Current); @@ -77,12 +141,6 @@ private IChangeSet Process(ChangeAwareList target, IChangeSet changes) private IChangeSet ProcessImpl(ChangeAwareList target, IChangeSet changes) { - if (_comparer == null) - { - target.Clone(changes); - return target.CaptureChanges(); - } - var refreshes = new List(changes.Refreshes); foreach (var change in changes) @@ -112,32 +170,31 @@ private IChangeSet ProcessImpl(ChangeAwareList target, IChangeSet chang } case ListChangeReason.Remove: - { - var current = change.Item.Current; - Remove(target, current); - break; - } + { + var current = change.Item.Current; + Remove(target, current); + break; + } case ListChangeReason.Refresh: - { - //add to refresh list so position can be calculated - refreshes.Add(change.Item.Current); + { + // add to refresh list so position can be calculated + refreshes.Add(change.Item.Current); - //add to current list so downstream operators can receive a refresh - //notification, so get the latest index and pass the index up the chain - var indexed = target - .IndexOfOptional(change.Item.Current) - .ValueOrThrow(() => new SortException($"Cannot find index of {typeof(T).Name} -> {change.Item.Current}. Expected to be in the list")); + // add to current list so downstream operators can receive a refresh + // notification, so get the latest index and pass the index up the chain + var indexed = target.IndexOfOptional(change.Item.Current).ValueOrThrow(() => new SortException($"Cannot find index of {typeof(T).Name} -> {change.Item.Current}. Expected to be in the list")); - target.Refresh(indexed.Item, indexed.Index); - break; - } + target.Refresh(indexed.Item, indexed.Index); + break; + } case ListChangeReason.Replace: { var current = change.Item.Current; - //TODO: check whether an item should stay in the same position - //i.e. update and move + + // TODO: check whether an item should stay in the same position + // i.e. update and move Remove(target, change.Item.Previous.Value); Insert(target, current); break; @@ -157,7 +214,7 @@ private IChangeSet ProcessImpl(ChangeAwareList target, IChangeSet chang } } - //Now deal with refreshes [can be expensive] + // Now deal with refreshes [can be expensive] foreach (var item in refreshes) { var old = target.IndexOf(item); @@ -166,39 +223,45 @@ private IChangeSet ProcessImpl(ChangeAwareList target, IChangeSet chang continue; } - int newposition = GetInsertPositionLinear(target, item); - if (old < newposition) + int newPosition = GetInsertPositionLinear(target, item); + if (old < newPosition) { - newposition--; + newPosition--; } - if (old == newposition) + if (old == newPosition) { continue; } - target.Move(old, newposition); + target.Move(old, newPosition); } return target.CaptureChanges(); } + private void Remove(ChangeAwareList target, T item) + { + var index = GetCurrentPosition(target, item); + target.RemoveAt(index); + } + private IChangeSet Reorder(ChangeAwareList target) { int index = -1; - var sorted = target.OrderBy(t => t, _comparer).ToList(); - foreach (var item in sorted) + foreach (var item in target.OrderBy(t => t, _comparer).ToList()) { index++; var existing = target[index]; - //if item is in the same place, + + // if item is in the same place, if (ReferenceEquals(item, existing)) { continue; } - //Cannot use binary search as Resort is implicit of a mutable change + // Cannot use binary search as Resort is implicit of a mutable change var old = target.IndexOf(item); target.Move(old, index); } @@ -206,20 +269,6 @@ private IChangeSet Reorder(ChangeAwareList target) return target.CaptureChanges(); } - private IChangeSet ChangeComparer(ChangeAwareList target, IComparer comparer) - { - _comparer = comparer; - if (_resetThreshold > 0 && target.Count <= _resetThreshold) - { - return Reorder(target); - } - - var sorted = target.OrderBy(t => t, _comparer).ToList(); - target.Clear(); - target.AddRange(sorted); - return target.CaptureChanges(); - } - private IChangeSet Reset(List original, ChangeAwareList target) { var sorted = original.OrderBy(t => t, _comparer).ToList(); @@ -227,65 +276,5 @@ private IChangeSet Reset(List original, ChangeAwareList target) target.AddRange(sorted); return target.CaptureChanges(); } - - private void Remove(ChangeAwareList target, T item) - { - var index = GetCurrentPosition(target, item); - target.RemoveAt(index); - } - - private void Insert(ChangeAwareList target, T item) - { - var index = GetInsertPosition(target, item); - target.Insert(index, item); - } - - private int GetInsertPosition(ChangeAwareList target, T item) - { - return _sortOptions == SortOptions.UseBinarySearch - ? GetInsertPositionBinary(target, item) - : GetInsertPositionLinear(target, item); - } - - private int GetInsertPositionLinear(ChangeAwareList target, T item) - { - for (var i = 0; i < target.Count; i++) - { - if (_comparer.Compare(item, target[i]) < 0) - { - return i; - } - } - - return target.Count; - } - - private int GetInsertPositionBinary(ChangeAwareList target, T item) - { - int index = target.BinarySearch(item, _comparer); - int insertIndex = ~index; - - //sort is not returning uniqueness - if (insertIndex < 0) - { - throw new SortException("Binary search has been specified, yet the sort does not yield uniqueness"); - } - - return insertIndex; - } - - private int GetCurrentPosition(ChangeAwareList target, T item) - { - var index = _sortOptions == SortOptions.UseBinarySearch - ? target.BinarySearch(item, _comparer) - : target.IndexOf(item); - - if (index < 0) - { - throw new SortException($"Cannot find item: {typeof(T).Name} -> {item}"); - } - - return index; - } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/SubscribeMany.cs b/src/DynamicData/List/Internal/SubscribeMany.cs index 66f6966ae..5e1b1cdd4 100644 --- a/src/DynamicData/List/Internal/SubscribeMany.cs +++ b/src/DynamicData/List/Internal/SubscribeMany.cs @@ -1,20 +1,20 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Annotations; namespace DynamicData.List.Internal { internal sealed class SubscribeMany { private readonly IObservable> _source; + private readonly Func _subscriptionFactory; - public SubscribeMany([NotNull] IObservable> source, [NotNull] Func subscriptionFactory) + public SubscribeMany(IObservable> source, Func subscriptionFactory) { _source = source ?? throw new ArgumentNullException(nameof(source)); _subscriptionFactory = subscriptionFactory ?? throw new ArgumentNullException(nameof(subscriptionFactory)); @@ -22,18 +22,14 @@ public SubscribeMany([NotNull] IObservable> source, [NotNull] Func public IObservable> Run() { - return Observable.Create> - ( - observer => + return Observable.Create>( + observer => { var shared = _source.Publish(); - var subscriptions = shared - .Transform(t => _subscriptionFactory(t)) - .DisposeMany() - .Subscribe(); + var subscriptions = shared.Transform(t => _subscriptionFactory(t)).DisposeMany().Subscribe(); return new CompositeDisposable(subscriptions, shared.SubscribeSafe(observer), shared.Connect()); }); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/Switch.cs b/src/DynamicData/List/Internal/Switch.cs index 93c88ee60..c9980f746 100644 --- a/src/DynamicData/List/Internal/Switch.cs +++ b/src/DynamicData/List/Internal/Switch.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -19,27 +19,26 @@ public Switch(IObservable>> sources) public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); - - var destination = new SourceList(); - - var populator = Observable.Switch(_sources - .Do(_ => + return Observable.Create>( + observer => { - lock (locker) - { - destination.Clear(); - } - })) - .Synchronize(locker) - .PopulateInto(destination); - - var publisher = destination.Connect().SubscribeSafe(observer); - return new CompositeDisposable(destination, populator, publisher); - }); + var locker = new object(); + + var destination = new SourceList(); + + var populator = Observable.Switch( + _sources.Do( + _ => + { + lock (locker) + { + destination.Clear(); + } + })).Synchronize(locker).PopulateInto(destination); + + var publisher = destination.Connect().SubscribeSafe(observer); + return new CompositeDisposable(destination, populator, publisher); + }); } - } } \ No newline at end of file diff --git a/src/DynamicData/List/Internal/ToObservableChangeSet.cs b/src/DynamicData/List/Internal/ToObservableChangeSet.cs index 86c8c33da..4b119ceeb 100644 --- a/src/DynamicData/List/Internal/ToObservableChangeSet.cs +++ b/src/DynamicData/List/Internal/ToObservableChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,29 +9,27 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal class ToObservableChangeSet { - private readonly IObservable> _source; - private readonly Func _expireAfter; + private readonly Func? _expireAfter; + private readonly int _limitSizeTo; + private readonly IScheduler _scheduler; - public ToObservableChangeSet(IObservable source, - Func expireAfter, - int limitSizeTo, - IScheduler scheduler = null) - : this(source.Select(t => new[] { t }), expireAfter, limitSizeTo, scheduler) + private readonly IObservable> _source; + + public ToObservableChangeSet(IObservable source, Func? expireAfter, int limitSizeTo, IScheduler? scheduler = null) + : this(source.Select(t => new[] { t }), expireAfter, limitSizeTo, scheduler) { } - public ToObservableChangeSet(IObservable> source, - Func expireAfter, - int limitSizeTo, - IScheduler scheduler = null) + public ToObservableChangeSet(IObservable> source, Func? expireAfter, int limitSizeTo, IScheduler? scheduler = null) { _source = source; _expireAfter = expireAfter; @@ -41,88 +39,81 @@ public ToObservableChangeSet(IObservable> source, public IObservable> Run() { - return Observable.Create>(observer => - { - if (_expireAfter == null && _limitSizeTo < 1) - { - return _source.Scan(new ChangeAwareList(), (state, latest) => - { - var items = latest.AsArray(); - if (items.Length == 1) - { - state.Add(items); - } - else - { - state.AddRange(items); - } - - return state; - }) - .Select(state => state.CaptureChanges()) - .SubscribeSafe(observer); - } - - long orderItemWasAdded = -1; - var locker = new object(); - - var sourceList = new ChangeAwareList>(); - - var sizeLimited = _source.Synchronize(locker) - .Scan(sourceList, (state, latest) => + return Observable.Create>( + observer => { - var items = latest.AsArray(); - var expirable = items.Select(t => CreateExpirableItem(t, ref orderItemWasAdded)); - - if (items.Length == 1) - { - sourceList.Add(expirable); - } - else + if (_expireAfter is null && _limitSizeTo < 1) { - sourceList.AddRange(expirable); + return _source.Scan( + new ChangeAwareList(), + (state, latest) => + { + var items = latest.AsArray(); + if (items.Length == 1) + { + state.Add(items); + } + else + { + state.AddRange(items); + } + + return state; + }).Select(state => state.CaptureChanges()).SubscribeSafe(observer); } - if (_limitSizeTo > 0 && state.Count > _limitSizeTo) - { - //remove oldest items [these will always be the first x in the list] - var toRemove = state.Count - _limitSizeTo; - state.RemoveRange(0, toRemove); - } - - return state; - }) - .Select(state => state.CaptureChanges()) - .Publish(); - - var timeLimited = (_expireAfter == null ? Observable.Never>>() : sizeLimited) - .Filter(ei => ei.ExpireAt != DateTime.MaxValue) - .GroupWithImmutableState(ei => ei.ExpireAt) - .MergeMany(grouping => - { - var expireAt = grouping.Key.Subtract(_scheduler.Now.DateTime); - return Observable.Timer(expireAt, _scheduler).Select(_ => grouping); - }) - .Synchronize(locker) - .Select(grouping => - { - sourceList.RemoveMany(grouping.Items); - return sourceList.CaptureChanges(); + long orderItemWasAdded = -1; + var locker = new object(); + + var sourceList = new ChangeAwareList>(); + + var sizeLimited = _source.Synchronize(locker).Scan( + sourceList, + (state, latest) => + { + var items = latest.AsArray(); + var expirable = items.Select(t => CreateExpirableItem(t, ref orderItemWasAdded)); + + if (items.Length == 1) + { + sourceList.Add(expirable); + } + else + { + sourceList.AddRange(expirable); + } + + if (_limitSizeTo > 0 && state.Count > _limitSizeTo) + { + // remove oldest items [these will always be the first x in the list] + var toRemove = state.Count - _limitSizeTo; + state.RemoveRange(0, toRemove); + } + + return state; + }).Select(state => state.CaptureChanges()).Publish(); + + var timeLimited = (_expireAfter is null ? Observable.Never>>() : sizeLimited).Filter(ei => ei.ExpireAt != DateTime.MaxValue).GroupWithImmutableState(ei => ei.ExpireAt).MergeMany( + grouping => + { + var expireAt = grouping.Key.Subtract(_scheduler.Now.DateTime); + return Observable.Timer(expireAt, _scheduler).Select(_ => grouping); + }).Synchronize(locker).Select( + grouping => + { + sourceList.RemoveMany(grouping.Items); + return sourceList.CaptureChanges(); + }); + + var publisher = sizeLimited.Merge(timeLimited).Cast(ei => ei.Item).NotEmpty().SubscribeSafe(observer); + + return new CompositeDisposable(publisher, sizeLimited.Connect()); }); - - var publisher = sizeLimited - .Merge(timeLimited) - .Cast(ei => ei.Item) - .NotEmpty() - .SubscribeSafe(observer); - - return new CompositeDisposable(publisher, sizeLimited.Connect()); - }); } private ExpirableItem CreateExpirableItem(T latest, ref long orderItemWasAdded) { - //check whether expiry has been set for any items + // check whether expiry has been set for any items var dateTime = _scheduler.Now.DateTime; var removeAt = _expireAfter?.Invoke(latest); var expireAt = removeAt.HasValue ? dateTime.Add(removeAt.Value) : DateTime.MaxValue; @@ -130,4 +121,4 @@ private ExpirableItem CreateExpirableItem(T latest, ref long orderItemWasAdde return new ExpirableItem(latest, expireAt, Interlocked.Increment(ref orderItemWasAdded)); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/TransformAsync.cs b/src/DynamicData/List/Internal/TransformAsync.cs index 5300b8dd8..a24b36a54 100644 --- a/src/DynamicData/List/Internal/TransformAsync.cs +++ b/src/DynamicData/List/Internal/TransformAsync.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,56 +8,54 @@ using System.Reactive.Linq; using System.Reactive.Threading.Tasks; using System.Threading.Tasks; -using DynamicData.Annotations; namespace DynamicData.List.Internal { internal class TransformAsync { - private readonly IObservable> _source; private readonly Func> _containerFactory; - public TransformAsync([NotNull] IObservable> source, [NotNull] Func> factory) + private readonly IObservable> _source; + + public TransformAsync(IObservable> source, Func> factory) { - if (factory == null) + if (factory is null) { throw new ArgumentNullException(nameof(factory)); } _source = source ?? throw new ArgumentNullException(nameof(source)); _containerFactory = async item => - { - var destination = await factory(item).ConfigureAwait(false); - return new TransformedItemContainer(item, destination); - }; + { + var destination = await factory(item).ConfigureAwait(false); + return new TransformedItemContainer(item, destination); + }; } public IObservable> Run() { - return Observable.Create>(observer => - { - var state = new ChangeAwareList(); - - var subscriber = _source.Select(async changes => + return Observable.Create>( + observer => { - await Transform(state, changes).ConfigureAwait(false); - return state; - }) - .Select(tasks => tasks.ToObservable()) - .SelectMany(items => items) - .Select(transformed => - { - var changed = transformed.CaptureChanges(); - return changed.Transform(container => container.Destination); - }).SubscribeSafe(observer); + var state = new ChangeAwareList(); - return subscriber; - }); + return _source.Select( + async changes => + { + await Transform(state, changes).ConfigureAwait(false); + return state; + }).Select(tasks => tasks.ToObservable()).SelectMany(items => items).Select( + transformed => + { + var changed = transformed.CaptureChanges(); + return changed.Transform(container => container.Destination); + }).SubscribeSafe(observer); + }); } private async Task Transform(ChangeAwareList transformed, IChangeSet changes) { - if (changes == null) + if (changes is null) { throw new ArgumentNullException(nameof(changes)); } @@ -67,185 +65,166 @@ private async Task Transform(ChangeAwareList transform switch (item.Reason) { case ListChangeReason.Add: - { - var change = item.Item; - if (change.CurrentIndex < 0 | change.CurrentIndex >= transformed.Count) - { - var container = await _containerFactory(item.Item.Current).ConfigureAwait(false); - transformed.Add(container); - } - else { - var container = await _containerFactory(item.Item.Current).ConfigureAwait(false); - transformed.Insert(change.CurrentIndex, container); - } + var change = item.Item; + if (change.CurrentIndex < 0 | change.CurrentIndex >= transformed.Count) + { + var container = await _containerFactory(item.Item.Current).ConfigureAwait(false); + transformed.Add(container); + } + else + { + var container = await _containerFactory(item.Item.Current).ConfigureAwait(false); + transformed.Insert(change.CurrentIndex, container); + } - break; - } + break; + } case ListChangeReason.AddRange: - { - var tasks = item.Range.Select(_containerFactory); - var containers = await Task.WhenAll(tasks).ConfigureAwait(false); - transformed.AddOrInsertRange(containers, item.Range.Index); - break; - } - - case ListChangeReason.Replace: - { - var change = item.Item; - var container = await _containerFactory(item.Item.Current).ConfigureAwait(false); - - if (change.CurrentIndex == change.PreviousIndex) - { - transformed[change.CurrentIndex] = container; - } - else { - transformed.RemoveAt(change.PreviousIndex); - transformed.Insert(change.CurrentIndex, container); + var tasks = item.Range.Select(_containerFactory); + var containers = await Task.WhenAll(tasks).ConfigureAwait(false); + transformed.AddOrInsertRange(containers, item.Range.Index); + break; } - break; - } + case ListChangeReason.Replace: + { + var change = item.Item; + var container = await _containerFactory(item.Item.Current).ConfigureAwait(false); - case ListChangeReason.Remove: - { - var change = item.Item; - bool hasIndex = change.CurrentIndex >= 0; + if (change.CurrentIndex == change.PreviousIndex) + { + transformed[change.CurrentIndex] = container; + } + else + { + transformed.RemoveAt(change.PreviousIndex); + transformed.Insert(change.CurrentIndex, container); + } - if (hasIndex) - { - transformed.RemoveAt(item.Item.CurrentIndex); + break; } - else + + case ListChangeReason.Remove: { - var toremove = transformed.FirstOrDefault(t => ReferenceEquals(t.Source, t)); + var change = item.Item; + bool hasIndex = change.CurrentIndex >= 0; - if (toremove != null) + if (hasIndex) + { + transformed.RemoveAt(item.Item.CurrentIndex); + } + else + { + var toRemove = transformed.FirstOrDefault(t => ReferenceEquals(t.Source, t)); + + if (toRemove is not null) { - transformed.Remove(toremove); + transformed.Remove(toRemove); } } - break; - } + break; + } case ListChangeReason.RemoveRange: - { - if (item.Range.Index >= 0) - { - transformed.RemoveRange(item.Range.Index, item.Range.Count); - } - else { - var toremove = transformed.Where(t => ReferenceEquals(t.Source, t)).ToArray(); - transformed.RemoveMany(toremove); - } + if (item.Range.Index >= 0) + { + transformed.RemoveRange(item.Range.Index, item.Range.Count); + } + else + { + var toRemove = transformed.Where(t => ReferenceEquals(t.Source, t)).ToArray(); + transformed.RemoveMany(toRemove); + } - break; - } + break; + } case ListChangeReason.Clear: - { - //i.e. need to store transformed reference so we can correctly clear - var toClear = new Change(ListChangeReason.Clear, transformed); - transformed.ClearOrRemoveMany(toClear); + { + // i.e. need to store transformed reference so we can correctly clear + var toClear = new Change(ListChangeReason.Clear, transformed); + transformed.ClearOrRemoveMany(toClear); - break; - } + break; + } case ListChangeReason.Moved: - { - var change = item.Item; - bool hasIndex = change.CurrentIndex >= 0; - if (!hasIndex) + { + var change = item.Item; + bool hasIndex = change.CurrentIndex >= 0; + if (!hasIndex) { throw new UnspecifiedIndexException("Cannot move as an index was not specified"); } - var collection = transformed as IExtendedList; - if (collection != null) - { - collection.Move(change.PreviousIndex, change.CurrentIndex); - } - else - { - var current = transformed[change.PreviousIndex]; - transformed.RemoveAt(change.PreviousIndex); - transformed.Insert(change.CurrentIndex, current); - } + if (transformed is IExtendedList collection) + { + collection.Move(change.PreviousIndex, change.CurrentIndex); + } + else + { + var current = transformed[change.PreviousIndex]; + transformed.RemoveAt(change.PreviousIndex); + transformed.Insert(change.CurrentIndex, current); + } - break; - } + break; + } } } } private class TransformedItemContainer : IEquatable { - public TSource Source { get; } - public TDestination Destination { get; } - public TransformedItemContainer(TSource source, TDestination destination) { Source = source; Destination = destination; } - #region Equality + public TDestination Destination { get; } - public bool Equals(TransformedItemContainer other) - { - if (ReferenceEquals(null, other)) - { - return false; - } + public TSource Source { get; } - if (ReferenceEquals(this, other)) - { - return true; - } + public static bool operator ==(TransformedItemContainer left, TransformedItemContainer right) + { + return Equals(left, right); + } - return EqualityComparer.Default.Equals(Source, other.Source); + public static bool operator !=(TransformedItemContainer left, TransformedItemContainer right) + { + return !Equals(left, right); } - public override bool Equals(object obj) + public bool Equals(TransformedItemContainer? other) { - if (ReferenceEquals(null, obj)) + if (ReferenceEquals(null, other)) { return false; } - if (ReferenceEquals(this, obj)) + if (ReferenceEquals(this, other)) { return true; } - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((TransformedItemContainer)obj); + return EqualityComparer.Default.Equals(Source, other.Source); } - public override int GetHashCode() + public override bool Equals(object? obj) { - return EqualityComparer.Default.GetHashCode(Source); + return obj is TransformedItemContainer item && Equals(item); } - public static bool operator ==(TransformedItemContainer left, TransformedItemContainer right) - { - return Equals(left, right); - } - - public static bool operator !=(TransformedItemContainer left, TransformedItemContainer right) + public override int GetHashCode() { - return !Equals(left, right); + return Source is null ? 0 : EqualityComparer.Default.GetHashCode(Source); } - - #endregion } } } \ No newline at end of file diff --git a/src/DynamicData/List/Internal/TransformMany.cs b/src/DynamicData/List/Internal/TransformMany.cs index 5f8bb2a5e..d0b49522b 100644 --- a/src/DynamicData/List/Internal/TransformMany.cs +++ b/src/DynamicData/List/Internal/TransformMany.cs @@ -1,284 +1,295 @@ -// Copyright (c) 2011-2019 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. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using DynamicData.Annotations; -using DynamicData.Binding; -using DynamicData.Kernel; - -namespace DynamicData.List.Internal -{ - internal sealed class TransformMany - { - private readonly IObservable> _source; - private readonly Func> _manyselector; - private readonly Func>> _childChanges; - private readonly IEqualityComparer _equalityComparer; - - public TransformMany(IObservable> source, - Func> manyselector, - IEqualityComparer equalityComparer = null) - : this(source, manyselector, equalityComparer, t => Observable.Defer(() => - { - var subsequentChanges = manyselector(t).ToObservableChangeSet(); - - if (manyselector(t).Count > 0) - { - return subsequentChanges; - } - - return Observable.Return(ChangeSet.Empty) - .Concat(subsequentChanges); - })) - { - } - - public TransformMany(IObservable> source, - Func> manyselector, - IEqualityComparer equalityComparer = null) - :this(source,manyselector, equalityComparer, t => Observable.Defer(() => - { - var subsequentChanges = manyselector(t).ToObservableChangeSet(); - - if (manyselector(t).Count > 0) - { - return subsequentChanges; - } - - return Observable.Return(ChangeSet.Empty) - .Concat(subsequentChanges); - })) - { - } - - public TransformMany(IObservable> source, - Func> manyselector, - IEqualityComparer equalityComparer = null) - : this(source, s => new ManySelectorFunc(s, x => manyselector(x).Items), equalityComparer, t => Observable.Defer(() => { - var subsequentChanges = manyselector(t).Connect(); - - if (manyselector(t).Count > 0) - { - return subsequentChanges; - } - - return Observable.Return(ChangeSet.Empty) - .Concat(subsequentChanges); - })) - { - } - - public TransformMany([NotNull] IObservable> source, - [NotNull] Func> manyselector, - IEqualityComparer equalityComparer = null, - Func>> childChanges = null) - { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _manyselector = manyselector; - _childChanges = childChanges; - _equalityComparer = equalityComparer ?? EqualityComparer.Default; - } - - public IObservable> Run() - { - if (_childChanges != null) - { - return CreateWithChangeset(); - } - - return Observable.Create>(observer => +// Copyright (c) 2011-2020 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. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; + +using DynamicData.Binding; +using DynamicData.Kernel; + +namespace DynamicData.List.Internal +{ + internal sealed class TransformMany + { + private readonly Func>>? _childChanges; + + private readonly IEqualityComparer _equalityComparer; + + private readonly Func> _manySelector; + + private readonly IObservable> _source; + + public TransformMany(IObservable> source, Func> manySelector, IEqualityComparer? equalityComparer = null) + : this( + source, + manySelector, + equalityComparer, + t => Observable.Defer( + () => + { + var subsequentChanges = manySelector(t).ToObservableChangeSet(); + + if (manySelector(t).Count > 0) + { + return subsequentChanges; + } + + return Observable.Return(ChangeSet.Empty).Concat(subsequentChanges); + })) + { + } + + public TransformMany(IObservable> source, Func> manySelector, IEqualityComparer? equalityComparer = null) + : this( + source, + manySelector, + equalityComparer, + t => Observable.Defer( + () => + { + var subsequentChanges = manySelector(t).ToObservableChangeSet(); + + if (manySelector(t).Count > 0) + { + return subsequentChanges; + } + + return Observable.Return(ChangeSet.Empty).Concat(subsequentChanges); + })) + { + } + + public TransformMany(IObservable> source, Func> manySelector, IEqualityComparer? equalityComparer = null) + : this( + source, + s => new ManySelectorFunc(s, x => manySelector(x).Items), + equalityComparer, + t => Observable.Defer( + () => + { + var subsequentChanges = manySelector(t).Connect(); + + if (manySelector(t).Count > 0) + { + return subsequentChanges; + } + + return Observable.Return(ChangeSet.Empty).Concat(subsequentChanges); + })) + { + } + + public TransformMany(IObservable> source, Func> manySelector, IEqualityComparer? equalityComparer = null, Func>>? childChanges = null) + { + _source = source ?? throw new ArgumentNullException(nameof(source)); + _manySelector = manySelector; + _childChanges = childChanges; + _equalityComparer = equalityComparer ?? EqualityComparer.Default; + } + + public IObservable> Run() + { + if (_childChanges is not null) { - //NB: ChangeAwareList is used internally by dd to capture changes to a list and ensure they can be replayed by subsequent operators - var result = new ChangeAwareList(); + return CreateWithChangeSet(); + } - return _source.Transform(item => new ManyContainer(_manyselector(item).ToArray()), true) - .Select(changes => + return Observable.Create>( + observer => { - var destinationChanges = new DestinationEnumerator(changes, _equalityComparer); - result.Clone(destinationChanges, _equalityComparer); - return result.CaptureChanges(); - }) + // NB: ChangeAwareList is used internally by dd to capture changes to a list and ensure they can be replayed by subsequent operators + var result = new ChangeAwareList(); - .NotEmpty() - .SubscribeSafe(observer); + return _source.Transform(item => new ManyContainer(_manySelector(item).ToArray()), true).Select( + changes => + { + var destinationChanges = new DestinationEnumerator(changes, _equalityComparer); + result.Clone(destinationChanges, _equalityComparer); + return result.CaptureChanges(); + }).NotEmpty().SubscribeSafe(observer); + }); + } + + private IObservable> CreateWithChangeSet() + { + if (_childChanges is null) + { + throw new InvalidOperationException("_childChanges must not be null."); } -); - } - - private IObservable> CreateWithChangeset() - { - return Observable.Create>(observer => - { - var result = new ChangeAwareList(); - - var transformed = _source.Transform(t => - { - var locker = new object(); - var collection = _manyselector(t); - var changes = _childChanges(t).Synchronize(locker).Skip(1); - return new ManyContainer(collection, changes); - }) - .Publish(); - - var outerLock = new object(); - var intial = transformed - .Synchronize(outerLock) - .Select(changes => new ChangeSet(new DestinationEnumerator(changes, _equalityComparer))); - - var subsequent = transformed - .MergeMany(x => x.Changes) - .Synchronize(outerLock); - - var init = intial.Select(changes => - { - result.Clone(changes, _equalityComparer); - return result.CaptureChanges(); - }); - - var subseq = subsequent - .RemoveIndex() - .Select(changes => - { - result.Clone(changes, _equalityComparer); - return result.CaptureChanges(); - }); - - var allChanges = init.Merge(subseq); - - return new CompositeDisposable(allChanges.SubscribeSafe(observer), transformed.Connect()); - }); - - } - - private sealed class ManyContainer - { - public IEnumerable Destination { get; } - public IObservable> Changes { get; } - - public ManyContainer(IEnumerable destination, IObservable> changes = null) - { - Destination = destination; - Changes = changes; - } - } - - //make this an instance - private sealed class DestinationEnumerator : IEnumerable> - { - private readonly IChangeSet _changes; - private readonly IEqualityComparer _equalityComparer; - - public DestinationEnumerator(IChangeSet changes, IEqualityComparer equalityComparer) - { - _changes = changes; - _equalityComparer = equalityComparer; - } - - public IEnumerator> GetEnumerator() - { - foreach (var change in _changes) - { - - switch (change.Reason) - { - case ListChangeReason.Add: - foreach (var destination in change.Item.Current.Destination) - { - yield return new Change(change.Reason, destination); - } - - break; - case ListChangeReason.AddRange: - { - var items = change.Range.SelectMany(m => m.Destination); - yield return new Change(change.Reason, items); - } - - break; - case ListChangeReason.Replace: - case ListChangeReason.Refresh: - { - //this is difficult as we need to discover adds and removes (and perhaps replaced) - var currentItems = change.Item.Current.Destination.AsArray(); - var previousItems = change.Item.Previous.Value.Destination.AsArray(); - - var adds = currentItems.Except(previousItems, _equalityComparer); - var removes = previousItems.Except(currentItems, _equalityComparer); - - //I am not sure whether it is possible to translate the original change into a replace - foreach (var destination in removes) - { - yield return new Change(ListChangeReason.Remove, destination); - } - - foreach (var destination in adds) - { - yield return new Change(ListChangeReason.Add, destination); - } - } - - break; - case ListChangeReason.Remove: - foreach (var destination in change.Item.Current.Destination) - { - yield return new Change(change.Reason, destination); - } - - break; - case ListChangeReason.RemoveRange: - { - var toRemove = change.Range.SelectMany(m => m.Destination); - - foreach (var destination in toRemove) - { - yield return new Change(ListChangeReason.Remove, destination); - } - } - - break; - case ListChangeReason.Moved: - //do nothing as the original index has no bearing on the destination index - break; - - case ListChangeReason.Clear: - { - var items = change.Range.SelectMany(m => m.Destination); - yield return new Change(change.Reason, items); - } - - break; - default: - throw new IndexOutOfRangeException("Unknown list reason " + change); - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } - - private class ManySelectorFunc : IEnumerable - { - private TSource _source; - private Func> _selector; - - public ManySelectorFunc(TSource source, Func> selector) - { - _source = source; - _selector = selector ?? throw new ArgumentNullException(nameof(selector)); - } - - public IEnumerator GetEnumerator() => _selector(_source).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _selector(_source).GetEnumerator(); - } - } -} + + return Observable.Create>( + observer => + { + var result = new ChangeAwareList(); + + var transformed = _source.Transform( + t => + { + var locker = new object(); + var collection = _manySelector(t); + var changes = _childChanges(t).Synchronize(locker).Skip(1); + return new ManyContainer(collection, changes); + }).Publish(); + + var outerLock = new object(); + var initial = transformed.Synchronize(outerLock).Select(changes => new ChangeSet(new DestinationEnumerator(changes, _equalityComparer))); + + var subsequent = transformed.MergeMany(x => x.Changes).Synchronize(outerLock); + + var init = initial.Select( + changes => + { + result.Clone(changes, _equalityComparer); + return result.CaptureChanges(); + }); + + var subsequentSelection = subsequent.RemoveIndex().Select( + changes => + { + result.Clone(changes, _equalityComparer); + return result.CaptureChanges(); + }); + + var allChanges = init.Merge(subsequentSelection); + + return new CompositeDisposable(allChanges.SubscribeSafe(observer), transformed.Connect()); + }); + } + + // make this an instance + private sealed class DestinationEnumerator : IEnumerable> + { + private readonly IChangeSet _changes; + + private readonly IEqualityComparer _equalityComparer; + + public DestinationEnumerator(IChangeSet changes, IEqualityComparer equalityComparer) + { + _changes = changes; + _equalityComparer = equalityComparer; + } + + public IEnumerator> GetEnumerator() + { + foreach (var change in _changes) + { + switch (change.Reason) + { + case ListChangeReason.Add: + foreach (var destination in change.Item.Current.Destination) + { + yield return new Change(change.Reason, destination); + } + + break; + + case ListChangeReason.AddRange: + { + var items = change.Range.SelectMany(m => m.Destination); + yield return new Change(change.Reason, items); + } + + break; + + case ListChangeReason.Replace: + case ListChangeReason.Refresh: + { + // this is difficult as we need to discover adds and removes (and perhaps replaced) + var currentItems = change.Item.Current.Destination.AsArray(); + var previousItems = change.Item.Previous.Value.Destination.AsArray(); + + var adds = currentItems.Except(previousItems, _equalityComparer); + + // I am not sure whether it is possible to translate the original change into a replace + foreach (var destination in previousItems.Except(currentItems, _equalityComparer)) + { + yield return new Change(ListChangeReason.Remove, destination); + } + + foreach (var destination in adds) + { + yield return new Change(ListChangeReason.Add, destination); + } + } + + break; + + case ListChangeReason.Remove: + foreach (var destination in change.Item.Current.Destination) + { + yield return new Change(change.Reason, destination); + } + + break; + + case ListChangeReason.RemoveRange: + { + foreach (var destination in change.Range.SelectMany(m => m.Destination)) + { + yield return new Change(ListChangeReason.Remove, destination); + } + } + + break; + + case ListChangeReason.Moved: + // do nothing as the original index has no bearing on the destination index + break; + + case ListChangeReason.Clear: + { + var items = change.Range.SelectMany(m => m.Destination); + yield return new Change(change.Reason, items); + } + + break; + + default: + throw new IndexOutOfRangeException("Unknown list reason " + change); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private sealed class ManyContainer + { + public ManyContainer(IEnumerable destination, IObservable>? changes = null) + { + Destination = destination; + Changes = changes ?? Observable.Empty>(); + } + + public IObservable> Changes { get; } + + public IEnumerable Destination { get; } + } + + private class ManySelectorFunc : IEnumerable + { + private readonly TSource _source; + + private readonly Func> _selector; + + public ManySelectorFunc(TSource source, Func> selector) + { + _source = source; + _selector = selector ?? throw new ArgumentNullException(nameof(selector)); + } + + public IEnumerator GetEnumerator() => _selector(_source).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _selector(_source).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/Transformer.cs b/src/DynamicData/List/Internal/Transformer.cs index 5261457b0..f6ef97085 100644 --- a/src/DynamicData/List/Internal/Transformer.cs +++ b/src/DynamicData/List/Internal/Transformer.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,20 +6,22 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class Transformer { + private readonly Func, int, TransformedItemContainer> _containerFactory; + private readonly IObservable> _source; - private readonly Func, int, TransformedItemContainer> _containerFactory; + private readonly bool _transformOnRefresh; - public Transformer([NotNull] IObservable> source, [NotNull] Func, int, TDestination> factory, bool transformOnRefresh) + public Transformer(IObservable> source, Func, int, TDestination> factory, bool transformOnRefresh) { - if (factory == null) + if (factory is null) { throw new ArgumentNullException(nameof(factory)); } @@ -31,87 +33,23 @@ public Transformer([NotNull] IObservable> source, [NotNull] public IObservable> Run() { - return _source.Scan(new ChangeAwareList(), (state, changes) => - { - Transform(state, changes); - return state; - }) - .Select(transformed => - { - var changed = transformed.CaptureChanges(); - return changed.Transform(container => container.Destination); - }); - } - - private sealed class TransformedItemContainer : IEquatable - { - public TSource Source { get; } - public TDestination Destination { get; } - - public TransformedItemContainer(TSource source, TDestination destination) - { - Source = source; - Destination = destination; - } - - #region Equality - - public bool Equals(TransformedItemContainer other) - { - if (ReferenceEquals(null, other)) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return EqualityComparer.Default.Equals(Source, other.Source); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((TransformedItemContainer)obj); - } - - public override int GetHashCode() - { - return EqualityComparer.Default.GetHashCode(Source); - } - - public static bool operator ==(TransformedItemContainer left, TransformedItemContainer right) - { - return Equals(left, right); - } - - public static bool operator !=(TransformedItemContainer left, TransformedItemContainer right) - { - return !Equals(left, right); - } - - #endregion + return _source.Scan( + new ChangeAwareList(), + (state, changes) => + { + Transform(state, changes); + return state; + }).Select( + transformed => + { + var changed = transformed.CaptureChanges(); + return changed.Transform(container => container.Destination); + }); } private void Transform(ChangeAwareList transformed, IChangeSet changes) { - if (changes == null) + if (changes is null) { throw new ArgumentNullException(nameof(changes)); } @@ -125,11 +63,11 @@ private void Transform(ChangeAwareList transformed, IC var change = item.Item; if (change.CurrentIndex < 0 | change.CurrentIndex >= transformed.Count) { - transformed.Add(_containerFactory(change.Current,Optional.None, transformed.Count)); + transformed.Add(_containerFactory(change.Current, Optional.None, transformed.Count)); } else { - var converted = _containerFactory(change.Current, Optional.None, change.CurrentIndex); + var converted = _containerFactory(change.Current, Optional.None, change.CurrentIndex); transformed.Insert(change.CurrentIndex, converted); } @@ -140,38 +78,33 @@ private void Transform(ChangeAwareList transformed, IC { var startIndex = item.Range.Index < 0 ? transformed.Count : item.Range.Index; - transformed.AddOrInsertRange(item.Range - .Select((t, idx) => _containerFactory(t, Optional.None, idx + startIndex)), - item.Range.Index); + transformed.AddOrInsertRange(item.Range.Select((t, idx) => _containerFactory(t, Optional.None, idx + startIndex)), item.Range.Index); break; } case ListChangeReason.Refresh: - { - if (_transformOnRefresh) { - var change = item.Item; - Optional previous = transformed[change.CurrentIndex].Destination; - var refreshed = _containerFactory(change.Current, previous, change.CurrentIndex); + if (_transformOnRefresh) + { + var change = item.Item; + Optional previous = transformed[change.CurrentIndex].Destination; + transformed[change.CurrentIndex] = _containerFactory(change.Current, previous, change.CurrentIndex); + } + else + { + transformed.RefreshAt(item.Item.CurrentIndex); + } - transformed[change.CurrentIndex] = refreshed; - } - else - { - transformed.RefreshAt(item.Item.CurrentIndex); + break; } - break; - } - case ListChangeReason.Replace: { var change = item.Item; Optional previous = transformed[change.PreviousIndex].Destination; if (change.CurrentIndex == change.PreviousIndex) { - transformed[change.CurrentIndex] = _containerFactory(change.Current, previous, change.CurrentIndex); } else @@ -194,9 +127,9 @@ private void Transform(ChangeAwareList transformed, IC } else { - var toRemove = transformed.FirstOrDefault(t => ReferenceEquals(t.Source, change.Current)); + TransformedItemContainer? toRemove = transformed.FirstOrDefault(t => ReferenceEquals(t.Source, change.Current)); - if (toRemove != null) + if (toRemove is not null) { transformed.Remove(toRemove); } @@ -222,7 +155,7 @@ private void Transform(ChangeAwareList transformed, IC case ListChangeReason.Clear: { - //i.e. need to store transformed reference so we can correctly clear + // i.e. need to store transformed reference so we can correctly clear var toClear = new Change(ListChangeReason.Clear, transformed); transformed.ClearOrRemoveMany(toClear); @@ -244,5 +177,68 @@ private void Transform(ChangeAwareList transformed, IC } } } + + private sealed class TransformedItemContainer : IEquatable + { + public TransformedItemContainer(TSource source, TDestination destination) + { + Source = source; + Destination = destination; + } + + public TDestination Destination { get; } + + public TSource Source { get; } + + public static bool operator ==(TransformedItemContainer left, TransformedItemContainer right) + { + return Equals(left, right); + } + + public static bool operator !=(TransformedItemContainer left, TransformedItemContainer right) + { + return !Equals(left, right); + } + + public bool Equals(TransformedItemContainer? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return EqualityComparer.Default.Equals(Source, other.Source); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((TransformedItemContainer)obj); + } + + public override int GetHashCode() + { + return Source is null ? 0 : EqualityComparer.Default.GetHashCode(Source); + } + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/UnifiedChange.cs b/src/DynamicData/List/Internal/UnifiedChange.cs index d05c1ec9c..acaa18fb5 100644 --- a/src/DynamicData/List/Internal/UnifiedChange.cs +++ b/src/DynamicData/List/Internal/UnifiedChange.cs @@ -1,19 +1,16 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal readonly struct UnifiedChange : IEquatable> { - public ListChangeReason Reason { get; } - public T Current { get; } - public Optional Previous { get; } - public UnifiedChange(ListChangeReason reason, T current) : this(reason, current, Optional.None()) { @@ -26,21 +23,35 @@ public UnifiedChange(ListChangeReason reason, T current, Optional previous) Previous = previous; } - #region Equality + public ListChangeReason Reason { get; } + + public T Current { get; } + + public Optional Previous { get; } + + public static bool operator ==(UnifiedChange left, UnifiedChange right) + { + return left.Equals(right); + } + + public static bool operator !=(UnifiedChange left, UnifiedChange right) + { + return !left.Equals(right); + } public bool Equals(UnifiedChange other) { return Reason == other.Reason && EqualityComparer.Default.Equals(Current, other.Current) && Previous.Equals(other.Previous); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { return false; } - return obj is UnifiedChange && Equals((UnifiedChange)obj); + return obj is UnifiedChange unifiedChange && Equals(unifiedChange); } public override int GetHashCode() @@ -48,27 +59,15 @@ public override int GetHashCode() unchecked { var hashCode = (int)Reason; - hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Current); + hashCode = (hashCode * 397) ^ (Current is null ? 0 : EqualityComparer.Default.GetHashCode(Current)); hashCode = (hashCode * 397) ^ Previous.GetHashCode(); return hashCode; } } - public static bool operator ==(UnifiedChange left, UnifiedChange right) - { - return left.Equals(right); - } - - public static bool operator !=(UnifiedChange left, UnifiedChange right) - { - return !left.Equals(right); - } - - #endregion - public override string ToString() { return $"Reason: {Reason}, Current: {Current}, Previous: {Previous}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Internal/Virtualiser.cs b/src/DynamicData/List/Internal/Virtualiser.cs index cc7fe62c4..2cf33df1d 100644 --- a/src/DynamicData/List/Internal/Virtualiser.cs +++ b/src/DynamicData/List/Internal/Virtualiser.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,17 +6,18 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; namespace DynamicData.List.Internal { internal sealed class Virtualiser { - private readonly IObservable> _source; private readonly IObservable _requests; - public Virtualiser([NotNull] IObservable> source, [NotNull] IObservable requests) + private readonly IObservable> _source; + + public Virtualiser(IObservable> source, IObservable requests) { _source = source ?? throw new ArgumentNullException(nameof(source)); _requests = requests ?? throw new ArgumentNullException(nameof(requests)); @@ -24,37 +25,34 @@ public Virtualiser([NotNull] IObservable> source, [NotNull] IObser public IObservable> Run() { - return Observable.Create>(observer => - { - var locker = new object(); - var all = new List(); - var virtualised = new ChangeAwareList(); + return Observable.Create>( + observer => + { + var locker = new object(); + var all = new List(); + var virtualised = new ChangeAwareList(); - IVirtualRequest parameters = new VirtualRequest(0, 25); + IVirtualRequest parameters = new VirtualRequest(0, 25); - var requestStream = _requests - .Synchronize(locker) - .Select(request => - { - parameters = request; - return CheckParamsAndVirtualise(all, virtualised, request); - }); + var requestStream = _requests.Synchronize(locker).Select( + request => + { + parameters = request; + return CheckParamsAndVirtualise(all, virtualised, request); + }); - var datachanged = _source - .Synchronize(locker) - .Select(changes => Virtualise(all, virtualised, parameters, changes)); + var dataChanged = _source.Synchronize(locker).Select(changes => Virtualise(all, virtualised, parameters, changes)); - //TODO: Remove this shared state stuff ie. _parameters - return requestStream.Merge(datachanged) - .Where(changes => changes != null && changes.Count != 0) - .Select(changes => new VirtualChangeSet(changes, new VirtualResponse(virtualised.Count, parameters.StartIndex, all.Count))) - .SubscribeSafe(observer); - }); + // TODO: Remove this shared state stuff ie. _parameters + return requestStream.Merge(dataChanged).Where(changes => changes is not null && changes.Count != 0) + .Select(x => x!) + .Select(changes => new VirtualChangeSet(changes, new VirtualResponse(virtualised.Count, parameters.StartIndex, all.Count))).SubscribeSafe(observer); + }); } - private static IChangeSet CheckParamsAndVirtualise(List all, ChangeAwareList virtualised, IVirtualRequest request) + private static IChangeSet? CheckParamsAndVirtualise(IList all, ChangeAwareList virtualised, IVirtualRequest? request) { - if (request == null || request.StartIndex < 0 || request.Size < 1) + if (request is null || request.StartIndex < 0 || request.Size < 1) { return null; } @@ -62,43 +60,40 @@ private static IChangeSet CheckParamsAndVirtualise(List all, ChangeAwareLi return Virtualise(all, virtualised, request); } - private static IChangeSet Virtualise(List all, ChangeAwareList virtualised, IVirtualRequest request, IChangeSet changeset = null) + private static IChangeSet Virtualise(IList all, ChangeAwareList virtualised, IVirtualRequest request, IChangeSet? changeSet = null) { - if (changeset != null) + if (changeSet is not null) { - all.Clone(changeset); + all.Clone(changeSet); } var previous = virtualised; - var current = all.Skip(request.StartIndex) - .Take(request.Size) - .ToList(); + var current = all.Skip(request.StartIndex).Take(request.Size).ToList(); var adds = current.Except(previous); var removes = previous.Except(current); virtualised.RemoveMany(removes); - adds.ForEach(t => - { - var index = current.IndexOf(t); - virtualised.Insert(index, t); - }); + adds.ForEach( + t => + { + var index = current.IndexOf(t); + virtualised.Insert(index, t); + }); - var moves = changeset.EmptyIfNull() - .Where(change => change.Reason == ListChangeReason.Moved - && change.MovedWithinRange(request.StartIndex, request.StartIndex + request.Size)); + var moves = changeSet.EmptyIfNull().Where(change => change.Reason == ListChangeReason.Moved && change.MovedWithinRange(request.StartIndex, request.StartIndex + request.Size)); foreach (var change in moves) { - //check whether an item has moved within the same page + // check whether an item has moved within the same page var currentIndex = change.Item.CurrentIndex - request.StartIndex; var previousIndex = change.Item.PreviousIndex - request.StartIndex; virtualised.Move(previousIndex, currentIndex); } - //find replaces [Is this ever the case that it can be reached] + // find replaces [Is this ever the case that it can be reached] for (var i = 0; i < current.Count; i++) { var currentItem = current[i]; @@ -116,4 +111,4 @@ private static IChangeSet Virtualise(List all, ChangeAwareList virtuali return virtualised.CaptureChanges(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ItemChange.cs b/src/DynamicData/List/ItemChange.cs index 120314537..e29fdcce0 100644 --- a/src/DynamicData/List/ItemChange.cs +++ b/src/DynamicData/List/ItemChange.cs @@ -1,70 +1,34 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; + using DynamicData.Kernel; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Container to describe a single change to a cache + /// Container to describe a single change to a cache. /// - /// + /// The type of the item. public readonly struct ItemChange : IEquatable> { /// - /// An empty change + /// An empty change. /// public static readonly ItemChange Empty; /// - /// The reason for the change - /// - public ListChangeReason Reason { get; } - - /// - /// The item which has changed - /// - public T Current { get; } - - /// - /// The current index - /// - public int CurrentIndex { get; } - - /// - /// The previous change. - /// - /// This is only when Reason==ChangeReason.Replace. - /// - public Optional Previous { get; } - - /// - /// The previous index. - /// - /// This is only when Reason==ChangeReason.Replace or ChangeReason.Move. - /// - public int PreviousIndex { get; } - - #region Construction - - /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The reason. /// The current. /// The previous. /// Value of the current. /// Value of the previous. - /// For ChangeReason.Add, a previous value cannot be specified - /// or - /// For ChangeReason.Change, must supply previous value - /// For ChangeReason.Add, a previous value cannot be specified - /// or - /// For ChangeReason.Change, must supply previous value public ItemChange(ListChangeReason reason, T current, Optional previous, int currentIndex = -1, int previousIndex = -1) : this() { @@ -91,35 +55,76 @@ public ItemChange(ListChangeReason reason, T current, int currentIndex) Previous = Optional.None; } - #endregion + /// + /// Gets the reason for the change. + /// + public ListChangeReason Reason { get; } - #region Equality + /// + /// Gets the item which has changed. + /// + public T Current { get; } + + /// + /// Gets the current index. + /// + public int CurrentIndex { get; } + + /// + /// Gets the previous change. + /// + /// This is only when Reason==ChangeReason.Replace. + /// + public Optional Previous { get; } + + /// + /// Gets the previous index. + /// + /// This is only when Reason==ChangeReason.Replace or ChangeReason.Move. + /// + public int PreviousIndex { get; } + + /// + /// Determines whether the specified objects are equal. + /// + /// The left hand side to compare. + /// The right hand side to compare. + /// If the two values are equal. + public static bool operator ==(ItemChange left, ItemChange right) => left.Equals(right); + + /// + /// Determines whether the specified objects are not equal. + /// + /// The left hand side to compare. + /// The right hand side to compare. + /// If the two values are not equal. + public static bool operator !=(ItemChange left, ItemChange right) => !left.Equals(right); /// /// Determines whether the specified object, is equal to this instance. /// /// The other. - /// + /// If the value is equal. public bool Equals(ItemChange other) { return EqualityComparer.Default.Equals(Current, other.Current) && CurrentIndex == other.CurrentIndex && Previous.Equals(other.Previous) && PreviousIndex == other.PreviousIndex; } /// - /// Determines whether the specified , is equal to this instance. + /// Determines whether the specified , is equal to this instance. /// - /// The to compare with this instance. + /// The to compare with this instance. /// - /// true if the specified is equal to this instance; otherwise, false. + /// true if the specified is equal to this instance; otherwise, false. /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { return false; } - return obj is ItemChange && Equals((ItemChange)obj); + return obj is ItemChange change && Equals(change); } /// @@ -132,7 +137,7 @@ public override int GetHashCode() { unchecked { - var hashCode = EqualityComparer.Default.GetHashCode(Current); + var hashCode = Current is null ? 0 : EqualityComparer.Default.GetHashCode(Current); hashCode = (hashCode * 397) ^ CurrentIndex; hashCode = (hashCode * 397) ^ Previous.GetHashCode(); hashCode = (hashCode * 397) ^ PreviousIndex; @@ -141,26 +146,14 @@ public override int GetHashCode() } /// - /// Determines whether the specified objects are equal - /// - public static bool operator ==(ItemChange left, ItemChange right) => left.Equals(right); - - /// - /// Determines whether the specified objects are not equal - /// - public static bool operator !=(ItemChange left, ItemChange right) => !left.Equals(right); - - #endregion - - /// - /// Returns a that represents this instance. + /// Returns a that represents this instance. /// /// - /// A that represents this instance. + /// A that represents this instance. /// public override string ToString() { return $"Current: {Current}, Previous: {Previous}"; } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Linq/AddKeyEnumerator.cs b/src/DynamicData/List/Linq/AddKeyEnumerator.cs index e4e77d88c..c20216998 100644 --- a/src/DynamicData/List/Linq/AddKeyEnumerator.cs +++ b/src/DynamicData/List/Linq/AddKeyEnumerator.cs @@ -1,21 +1,21 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections; using System.Collections.Generic; -using DynamicData.Annotations; namespace DynamicData.List.Linq { internal class AddKeyEnumerator : IEnumerable> + where TKey : notnull { - private readonly IChangeSet _source; private readonly Func _keySelector; - public AddKeyEnumerator([NotNull] IChangeSet source, - [NotNull] Func keySelector) + private readonly IChangeSet _source; + + public AddKeyEnumerator(IChangeSet source, Func keySelector) { _source = source ?? throw new ArgumentNullException(nameof(source)); _keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector)); @@ -25,9 +25,8 @@ public AddKeyEnumerator([NotNull] IChangeSet source, /// Returns an enumerator that iterates through the collection. /// /// - /// A that can be used to iterate through the collection. + /// A that can be used to iterate through the collection. /// - /// public IEnumerator> GetEnumerator() { foreach (var change in _source) @@ -56,7 +55,7 @@ public IEnumerator> GetEnumerator() case ListChangeReason.Replace: { - //replace is a remove and add + // replace is a remove and add var previous = change.Item.Previous.Value; var previousKey = _keySelector(previous); yield return new Change(ChangeReason.Remove, previousKey, previous); @@ -107,4 +106,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Linq/ItemChangeEnumerator.cs b/src/DynamicData/List/Linq/ItemChangeEnumerator.cs index 927a0c397..e88e82faf 100644 --- a/src/DynamicData/List/Linq/ItemChangeEnumerator.cs +++ b/src/DynamicData/List/Linq/ItemChangeEnumerator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -29,8 +29,7 @@ public IEnumerator> GetEnumerator() } else { - int index; - index = change.Range.Index == -1 ? lastKnownIndex : change.Range.Index; + var index = change.Range.Index == -1 ? lastKnownIndex : change.Range.Index; foreach (var item in change.Range) { @@ -39,12 +38,15 @@ public IEnumerator> GetEnumerator() case ListChangeReason.AddRange: yield return new ItemChange(ListChangeReason.Add, item, index); break; + case ListChangeReason.RemoveRange: yield return new ItemChange(ListChangeReason.Remove, item, index); break; + case ListChangeReason.Clear: yield return new ItemChange(ListChangeReason.Remove, item, index); break; + default: yield break; } @@ -61,4 +63,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Linq/ReverseEnumerator.cs b/src/DynamicData/List/Linq/Reverser.cs similarity index 97% rename from src/DynamicData/List/Linq/ReverseEnumerator.cs rename to src/DynamicData/List/Linq/Reverser.cs index 592c2b277..7f1100995 100644 --- a/src/DynamicData/List/Linq/ReverseEnumerator.cs +++ b/src/DynamicData/List/Linq/Reverser.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -75,4 +75,4 @@ public IEnumerable> Reverse(IChangeSet changes) } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Linq/UnifiedChangeEnumerator.cs b/src/DynamicData/List/Linq/UnifiedChangeEnumerator.cs index e299ce8d3..f871edf09 100644 --- a/src/DynamicData/List/Linq/UnifiedChangeEnumerator.cs +++ b/src/DynamicData/List/Linq/UnifiedChangeEnumerator.cs @@ -1,9 +1,10 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections; using System.Collections.Generic; + using DynamicData.List.Internal; namespace DynamicData.List.Linq @@ -34,12 +35,15 @@ public IEnumerator> GetEnumerator() case ListChangeReason.AddRange: yield return new UnifiedChange(ListChangeReason.Add, item); break; + case ListChangeReason.RemoveRange: yield return new UnifiedChange(ListChangeReason.Remove, item); break; + case ListChangeReason.Clear: yield return new UnifiedChange(ListChangeReason.Clear, item); break; + default: yield break; } @@ -53,4 +57,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Linq/WithoutIndexEnumerator.cs b/src/DynamicData/List/Linq/WithoutIndexEnumerator.cs index f2d75808c..0ecb94ae7 100644 --- a/src/DynamicData/List/Linq/WithoutIndexEnumerator.cs +++ b/src/DynamicData/List/Linq/WithoutIndexEnumerator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -9,9 +9,9 @@ namespace DynamicData.List.Linq { /// /// Index to remove the index. This is necessary for WhereReasonAre* operators. - /// Otherwise these operators could break subsequent operators when the subsequent operator relies on the index + /// Otherwise these operators could break subsequent operators when the subsequent operator relies on the index. /// - /// + /// The type of the item. internal class WithoutIndexEnumerator : IEnumerable> { private readonly IEnumerable> _changeSet; @@ -27,7 +27,7 @@ public IEnumerator> GetEnumerator() { if (change.Reason == ListChangeReason.Moved) { - //exceptional case - makes no sense to remove index from move + // exceptional case - makes no sense to remove index from move continue; } @@ -47,4 +47,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ListChangeReason.cs b/src/DynamicData/List/ListChangeReason.cs index 664c4b111..618f1960f 100644 --- a/src/DynamicData/List/ListChangeReason.cs +++ b/src/DynamicData/List/ListChangeReason.cs @@ -1,4 +1,7 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// @@ -9,27 +12,27 @@ namespace DynamicData public enum ListChangeReason { /// - /// An item has been added + /// An item has been added. /// Add, /// - /// A range of items has been added + /// A range of items has been added. /// AddRange, /// - /// An item has been replaced + /// An item has been replaced. /// Replace, /// - /// An item has removed + /// An item has removed. /// Remove, /// - /// A range of items has been removed + /// A range of items has been removed. /// RemoveRange, @@ -39,13 +42,13 @@ public enum ListChangeReason Refresh, /// - /// An item has been moved in a sorted collection + /// An item has been moved in a sorted collection. /// Moved, /// - /// The entire collection has been cleared + /// The entire collection has been cleared. /// Clear, } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ListEx.cs b/src/DynamicData/List/ListEx.cs index 3be373719..bc1ad2f74 100644 --- a/src/DynamicData/List/ListEx.cs +++ b/src/DynamicData/List/ListEx.cs @@ -1,281 +1,167 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; -using DynamicData.Annotations; + using DynamicData.Kernel; -using System.Collections.ObjectModel; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Extensions to help with maintainence of a list + /// Extensions to help with maintenance of a list. /// public static class ListEx { - #region Apply operators to a list - - internal static bool MovedWithinRange(this Change source, int startIndex, int endIndex) - { - if (source.Reason != ListChangeReason.Moved) - { - return false; - } - - var current = source.Item.CurrentIndex; - var previous = source.Item.PreviousIndex; - - return current >= startIndex && current <= endIndex - || previous >= startIndex && previous <= endIndex; - } - /// - /// Clones the list from the specified change set + /// Adds the items to the specified list. /// - /// + /// The type of the item. /// The source. - /// The changes. + /// The items. /// /// source /// or - /// changes + /// items. /// - public static void Clone(this IList source, IChangeSet changes) + public static void Add(this IList source, IEnumerable items) { - Clone(source, changes, null); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - /// - /// Clones the list from the specified change set - /// - /// - /// The source. - /// The changes. - /// An equality comparer to match items in the changes. - /// - /// source - /// or - /// changes - /// - public static void Clone(this IList source, IChangeSet changes, IEqualityComparer equalityComparer) - { - Clone(source, (IEnumerable>)changes, equalityComparer); + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + items.ForEach(source.Add); } /// - /// Clones the list from the specified enumerable of changes + /// Adds the range if a negative is specified, otherwise the range is added at the end of the list. /// - /// + /// The type of the item. /// The source. - /// The changes. - /// An equality comparer to match items in the changes. - /// - /// source - /// or - /// changes - /// - public static void Clone(this IList source, IEnumerable> changes, IEqualityComparer equalityComparer) + /// The items. + /// The index. + public static void AddOrInsertRange(this IList source, IEnumerable items, int index) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (changes == null) - { - throw new ArgumentNullException(nameof(changes)); - } - - foreach (var item in changes) + if (items is null) { - Clone(source, item, equalityComparer ?? EqualityComparer.Default); + throw new ArgumentNullException(nameof(items)); } - } - - private static void Clone(this IList source, Change item, IEqualityComparer equalityComparer) - { - var changeAware = source as ChangeAwareList; - switch (item.Reason) + switch (source) { - case ListChangeReason.Add: - { - var change = item.Item; - var hasIndex = change.CurrentIndex >= 0; - if (hasIndex) - { - source.Insert(change.CurrentIndex, change.Current); - } - else - { - source.Add(change.Current); - } - + case List list when index >= 0: + list.InsertRange(index, items); break; - } - - case ListChangeReason.AddRange: - { - source.AddOrInsertRange(item.Range, item.Range.Index); + case List list: + list.AddRange(items); break; - } - - case ListChangeReason.Clear: - { - source.ClearOrRemoveMany(item); + case IExtendedList extendedList when index >= 0: + extendedList.InsertRange(items, index); break; - } - - case ListChangeReason.Replace: - { - var change = item.Item; - if (change.CurrentIndex >= 0 && change.CurrentIndex == change.PreviousIndex) - { - source[change.CurrentIndex] = change.Current; - } - else + case IExtendedList extendedList: + extendedList.AddRange(items); + break; + default: { - if (change.PreviousIndex == -1) - { - source.Remove(change.Previous.Value); - } - else - { - //is this best? or replace + move? - source.RemoveAt(change.PreviousIndex); - } - - if (change.CurrentIndex == -1) + if (index >= 0) { - source.Add(change.Current); + // TODO: Why the hell reverse? Surely there must be as reason otherwise I would not have done it. + items.Reverse().ForEach(t => source.Insert(index, t)); } else { - source.Insert(change.CurrentIndex, change.Current); + items.ForEach(source.Add); } + break; } + } + } - break; - } - - case ListChangeReason.Refresh: - { - if (changeAware != null) - { - changeAware.RefreshAt(item.Item.CurrentIndex); - } - else - { - source.RemoveAt(item.Item.CurrentIndex); - source.Insert(item.Item.CurrentIndex, item.Item.Current); - } - - break; - } + /// + /// Adds the range to the source ist. + /// + /// The type of the item. + /// The source. + /// The items. + /// + /// source + /// or + /// items. + /// + public static void AddRange(this IList source, IEnumerable items) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - case ListChangeReason.Remove: - { - var change = item.Item; - bool hasIndex = change.CurrentIndex >= 0; - if (hasIndex) - { - source.RemoveAt(change.CurrentIndex); - } - else - { - if (equalityComparer != null) - { - int index = source.IndexOf(change.Current, equalityComparer); - if (index > -1) - { - source.RemoveAt(index); - } - } - else - { - source.Remove(change.Current); - } - } + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + switch (source) + { + case List list: + list.AddRange(items); break; - } - - case ListChangeReason.RemoveRange: - { - //ignore this case because WhereReasonsAre removes the index [in which case call RemoveMany] - //if (item.Range.Index < 0) - // throw new UnspecifiedIndexException("ListChangeReason.RemoveRange should not have an index specified index"); - - if (item.Range.Index >= 0 && (source is IExtendedList || source is List)) - { - source.RemoveRange(item.Range.Index, item.Range.Count); - } - else - { - source.RemoveMany(item.Range); - } - + case IExtendedList extendedList: + extendedList.AddRange(items); break; - } - - case ListChangeReason.Moved: - { - var change = item.Item; - bool hasIndex = change.CurrentIndex >= 0; - if (!hasIndex) - { - throw new UnspecifiedIndexException("Cannot move as an index was not specified"); - } - - if (source is IExtendedList extendedList) - { - extendedList.Move(change.PreviousIndex, change.CurrentIndex); - } - else if (source is ObservableCollection observableCollection) - { - observableCollection.Move(change.PreviousIndex, change.CurrentIndex); - } - else - { - //check this works whatever the index is - source.RemoveAt(change.PreviousIndex); - source.Insert(change.CurrentIndex, change.Current); - } - + default: + items.ForEach(source.Add); break; - } } } /// - /// Clears the collection if the number of items in the range is the same as the source collection. Otherwise a remove many operation is applied. - /// - /// NB: This is because an observable change set may be a composite of multiple change sets in which case if one of them has clear operation applied it should not clear the entire result. + /// Adds the range to the list. The starting range is at the specified index. /// - /// + /// The type of the item. /// The source. - /// The change. - internal static void ClearOrRemoveMany(this IList source, Change change) + /// The items. + /// The index. + public static void AddRange(this IList source, IEnumerable items, int index) { - //apply this to other operators - if (source.Count == change.Range.Count) + if (source is null) { - source.Clear(); + throw new ArgumentNullException(nameof(source)); } - else + + if (items is null) { - source.RemoveMany(change.Range); + throw new ArgumentNullException(nameof(items)); } - } - #endregion - - #region Binary Search / Lookup + switch (source) + { + case List list: + list.InsertRange(index, items); + break; + case IExtendedList list: + list.InsertRange(items, index); + break; + default: + items.ForEach(source.Add); + break; + } + } /// /// Performs a binary search on the specified collection. @@ -283,7 +169,7 @@ internal static void ClearOrRemoveMany(this IList source, Change change /// The type of the item. /// The list to be searched. /// The value to search for. - /// + /// The index of the specified value in the specified array, if value is found; otherwise, a negative number. public static int BinarySearch(this IList list, TItem value) { return BinarySearch(list, value, Comparer.Default); @@ -296,10 +182,10 @@ public static int BinarySearch(this IList list, TItem value) /// The list to be searched. /// The value to search for. /// The comparer that is used to compare the value with the list items. - /// + /// The index of the specified value in the specified array, if value is found; otherwise, a negative number. public static int BinarySearch(this IList list, TItem value, IComparer comparer) { - if (comparer == null) + if (comparer is null) { throw new ArgumentNullException(nameof(comparer)); } @@ -310,22 +196,22 @@ public static int BinarySearch(this IList list, TItem value, IComp /// /// Performs a binary search on the specified collection. /// - /// Thanks to http://stackoverflow.com/questions/967047/how-to-perform-a-binary-search-on-ilistt + /// Thanks to http://stackoverflow.com/questions/967047/how-to-perform-a-binary-search-on-ilistt. /// /// The type of the item. /// The type of the searched item. /// The list to be searched. /// The value to search for. /// The comparer that is used to compare the value with the list items. - /// + /// The index of the specified value in the specified array, if value is found; otherwise, a negative number. public static int BinarySearch(this IList list, TSearch value, Func comparer) { - if (list == null) + if (list is null) { throw new ArgumentNullException(nameof(list)); } - if (comparer == null) + if (comparer is null) { throw new ArgumentNullException(nameof(comparer)); } @@ -335,7 +221,7 @@ public static int BinarySearch(this IList list, TSearch v while (lower <= upper) { - int middle = lower + (upper - lower) / 2; + int middle = lower + ((upper - lower) / 2); int comparisonResult = comparer(value, list[middle]); if (comparisonResult < 0) { @@ -355,48 +241,96 @@ public static int BinarySearch(this IList list, TSearch v } /// - /// Lookups the item using the specified comparer. If matched, the item's index is also returned + /// Clones the list from the specified change set. /// - /// + /// The type of the item. /// The source. - /// The item. - /// The equality comparer. - /// - public static Optional> IndexOfOptional(this IEnumerable source, T item, IEqualityComparer equalityComparer = null) + /// The changes. + /// + /// source + /// or + /// changes. + /// + public static void Clone(this IList source, IChangeSet changes) { - var comparer = equalityComparer ?? EqualityComparer.Default; - var index = source.IndexOf(item, comparer); - return index<0 ? Optional>.None : new ItemWithIndex(item,index); + Clone(source, changes, null); + } + + /// + /// Clones the list from the specified change set. + /// + /// The type of the item. + /// The source. + /// The changes. + /// An equality comparer to match items in the changes. + /// + /// source + /// or + /// changes. + /// + public static void Clone(this IList source, IChangeSet changes, IEqualityComparer? equalityComparer) + { + Clone(source, (IEnumerable>)changes, equalityComparer); + } + + /// + /// Clones the list from the specified enumerable of changes. + /// + /// The type of the item. + /// The source. + /// The changes. + /// An equality comparer to match items in the changes. + /// + /// source + /// or + /// changes. + /// + public static void Clone(this IList source, IEnumerable> changes, IEqualityComparer? equalityComparer) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (changes is null) + { + throw new ArgumentNullException(nameof(changes)); + } + + foreach (var item in changes) + { + Clone(source, item, equalityComparer ?? EqualityComparer.Default); + } } /// - /// Finds the index of the current item using the specified equality comparer + /// Finds the index of the current item using the specified equality comparer. /// - /// - /// - /// - /// + /// The type of the item. + /// The source enumerable. + /// The item to get the index of. + /// The index. public static int IndexOf(this IEnumerable source, T item) { return IndexOf(source, item, EqualityComparer.Default); } /// - /// Finds the index of the current item using the specified equality comparer + /// Finds the index of the current item using the specified equality comparer. /// - /// - /// - /// Use to determine object equality - /// - /// + /// The type of the item. + /// The source enumerable. + /// The item to get the index of. + /// Use to determine object equality. + /// The index. public static int IndexOf(this IEnumerable source, T item, IEqualityComparer equalityComparer) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (equalityComparer == null) + if (equalityComparer is null) { throw new ArgumentNullException(nameof(equalityComparer)); } @@ -415,388 +349,415 @@ public static int IndexOf(this IEnumerable source, T item, IEqualityCompar return -1; } - #endregion - - #region Amendment + /// + /// Lookups the item using the specified comparer. If matched, the item's index is also returned. + /// + /// The type of the item. + /// The source. + /// The item. + /// The equality comparer. + /// The index of the item if available. + public static Optional> IndexOfOptional(this IEnumerable source, T item, IEqualityComparer? equalityComparer = null) + { + var comparer = equalityComparer ?? EqualityComparer.Default; + var index = source.IndexOf(item, comparer); + return index < 0 ? Optional>.None : new ItemWithIndex(item, index); + } /// - /// Adds the items to the specified list + /// Removes the items from the specified list. /// - /// + /// The type of the item. /// The source. /// The items. /// /// source /// or - /// items + /// items. /// - public static void Add(this IList source, IEnumerable items) + public static void Remove(this IList source, IEnumerable items) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (items == null) + if (items is null) { throw new ArgumentNullException(nameof(items)); } - items.ForEach(source.Add); + items.ForEach(t => source.Remove(t)); } /// - /// Adds the range to the source ist + /// Removes many items from the collection in an optimal way. /// - /// - /// The source. - /// The items. - /// - /// source - /// or - /// items - /// - public static void AddRange(this IList source, IEnumerable items) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - - if (source is List) - { - ((List)source).AddRange(items); - } - else if (source is IExtendedList) - { - ((IExtendedList)source).AddRange(items); - } - else - { - items.ForEach(source.Add); - } - } - - /// - /// Adds the range to the list. The starting range is at the specified index - /// - /// - /// The source. - /// The items. - /// The index. - /// - /// - public static void AddRange(this IList source, IEnumerable items, int index) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - - if (source is List) - { - ((List)source).InsertRange(index, items); - } - else if (source is IExtendedList) - { - ((IExtendedList)source).InsertRange(items, index); - } - else - { - items.ForEach(source.Add); - } - } - - /// - /// Adds the range if a negative is specified, otherwise the range is added at the end of the list - /// - /// - /// The source. - /// The items. - /// The index. - /// - /// - public static void AddOrInsertRange(this IList source, IEnumerable items, int index) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - - if (source is List) - { - if (index >= 0) - { - ((List)source).InsertRange(index, items); - } - else - { - ((List)source).AddRange(items); - } - } - else if (source is IExtendedList) - { - if (index >= 0) - { - ((IExtendedList)source).InsertRange(items, index); - } - else - { - ((IExtendedList)source).AddRange(items); - } - } - else - { - if (index >= 0) - { - //TODO: Why the hell reverse? Surely there must be as reason otherwise I would not have done it. - items.Reverse().ForEach(t => source.Insert(index, t)); - } - else - { - items.ForEach(source.Add); - } - } - } - - /// - /// Removes many items from the collection in an optimal way - /// - /// + /// The type of the item. /// The source. /// The items to remove. - /// - /// - public static void RemoveMany(this IList source, [NotNull] IEnumerable itemsToRemove) + public static void RemoveMany(this IList source, IEnumerable itemsToRemove) { /* This may seem OTT but for large sets of data where there are many removes scattered across the source collection IndexOf lookups can result in very slow updates (especially for subsequent operators) */ - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (itemsToRemove == null) + if (itemsToRemove is null) { throw new ArgumentNullException(nameof(itemsToRemove)); } var toRemoveArray = itemsToRemove.AsArray(); - //match all indicies and and remove in reverse as it is more efficient - var toRemove = source.IndexOfMany(toRemoveArray) - .OrderByDescending(x => x.Index) - .ToArray(); + // match all indexes and and remove in reverse as it is more efficient + var toRemove = source.IndexOfMany(toRemoveArray).OrderByDescending(x => x.Index).ToArray(); - //if there are duplicates, it could be that an item exists in the - //source collection more than once - in that case the fast remove - //would remove each instance + // if there are duplicates, it could be that an item exists in the + // source collection more than once - in that case the fast remove + // would remove each instance var hasDuplicates = toRemove.Duplicates(t => t.Item).Any(); if (hasDuplicates) { - //Slow remove but safe - toRemoveArray?.ForEach(t => source.Remove(t)); + // Slow remove but safe + toRemoveArray.ForEach(t => source.Remove(t)); } else { - //Fast remove because we know the index of all and we remove in order + // Fast remove because we know the index of all and we remove in order toRemove.ForEach(t => source.RemoveAt(t.Index)); } } /// - /// Removes the number of items, starting at the specified index + /// Replaces the specified item. /// - /// + /// The type of the item. /// The source. - /// The index. - /// The count. - /// - /// Cannot remove range - private static void RemoveRange(this IList source, int index, int count) + /// The original. + /// The value to replace with. + /// source + /// or + /// items. + public static void Replace(this IList source, T original, T replaceWith) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (source is List) - { - ((List)source).RemoveRange(index, count); - } - else if (source is IExtendedList) - { - ((IExtendedList)source).RemoveRange(index, count); - } - else + if (original is null) { - throw new NotSupportedException($"Cannot remove range from {source.GetType().FullName}"); + throw new ArgumentNullException(nameof(original)); } - } - /// - /// Removes the items from the specified list - /// - /// - /// The source. - /// The items. - /// - /// source - /// or - /// items - /// - public static void Remove(this IList source, IEnumerable items) - { - if (source == null) + if (replaceWith is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(replaceWith)); } - if (items == null) + var index = source.IndexOf(original); + if (index == -1) { - throw new ArgumentNullException(nameof(items)); + throw new ArgumentException("Cannot find index of original item. Either it does not exist in the list or the hashcode has mutated"); } - items.ForEach(t => source.Remove(t)); + source[index] = replaceWith; } /// /// Replaces the specified item. /// - /// + /// The type of item. /// The source. - /// The original. - /// The replacewith. + /// The item which is to be replaced. If not in the list and argument exception will be thrown. + /// The new item. + /// The equality comparer to be used to find the original item in the list. /// source /// or - /// items - public static void Replace(this IList source, [NotNull] T original, [NotNull] T replacewith) + /// items. + public static void Replace(this IList source, T original, T replaceWith, IEqualityComparer comparer) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (original == null) + if (original is null) { throw new ArgumentNullException(nameof(original)); } - if (replacewith == null) + if (replaceWith is null) { - throw new ArgumentNullException(nameof(replacewith)); + throw new ArgumentNullException(nameof(replaceWith)); + } + + if (comparer is null) + { + throw new ArgumentNullException(nameof(comparer)); } var index = source.IndexOf(original); - if (index==-1) + if (index == -1) { throw new ArgumentException("Cannot find index of original item. Either it does not exist in the list or the hashcode has mutated"); } - source[index] = replacewith; + if (comparer.Equals(source[index], replaceWith)) + { + source[index] = replaceWith; + } } /// - /// Replaces the item if found, otherwise the item is added to the list + /// Replaces the item if found, otherwise the item is added to the list. /// - /// + /// The type of the item. /// The source. /// The original. - /// The replacewith. - /// - /// - public static void ReplaceOrAdd(this IList source, [NotNull] T original, [NotNull] T replacewith) + /// The value to replace with. + public static void ReplaceOrAdd(this IList source, T original, T replaceWith) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (original == null) + if (original is null) { throw new ArgumentNullException(nameof(original)); } - if (replacewith == null) + if (replaceWith is null) { - throw new ArgumentNullException(nameof(replacewith)); + throw new ArgumentNullException(nameof(replaceWith)); } var index = source.IndexOf(original); if (index == -1) { - source.Add(replacewith); + source.Add(replaceWith); } else { - source[index] = replacewith; + source[index] = replaceWith; } - } /// - /// Replaces the specified item. + /// Clears the collection if the number of items in the range is the same as the source collection. Otherwise a remove many operation is applied. + /// + /// NB: This is because an observable change set may be a composite of multiple change sets in which case if one of them has clear operation applied it should not clear the entire result. /// + /// The type of the item. /// The source. - /// The item which is to be replaced. If not in the list and argument exception will be thrown - /// The new item - /// The equality comparer to be used to find the original item in the list - /// source - /// or - /// items - public static void Replace(this IList source, [NotNull] T original, [NotNull] T replaceWith, IEqualityComparer comparer) + /// The change. + internal static void ClearOrRemoveMany(this IList source, Change change) { - if (source == null) + // apply this to other operators + if (source.Count == change.Range.Count) { - throw new ArgumentNullException(nameof(source)); + source.Clear(); } - - if (original == null) + else { - throw new ArgumentNullException(nameof(original)); + source.RemoveMany(change.Range); } + } - if (replaceWith == null) + internal static bool MovedWithinRange(this Change source, int startIndex, int endIndex) + { + if (source.Reason != ListChangeReason.Moved) { - throw new ArgumentNullException(nameof(replaceWith)); + return false; } - if (comparer == null) + var current = source.Item.CurrentIndex; + var previous = source.Item.PreviousIndex; + + return (current >= startIndex && current <= endIndex) || (previous >= startIndex && previous <= endIndex); + } + + private static void Clone(this IList source, Change item, IEqualityComparer equalityComparer) + { + var changeAware = source as ChangeAwareList; + + switch (item.Reason) { - throw new ArgumentNullException(nameof(comparer)); + case ListChangeReason.Add: + { + var change = item.Item; + var hasIndex = change.CurrentIndex >= 0; + if (hasIndex) + { + source.Insert(change.CurrentIndex, change.Current); + } + else + { + source.Add(change.Current); + } + + break; + } + + case ListChangeReason.AddRange: + { + source.AddOrInsertRange(item.Range, item.Range.Index); + break; + } + + case ListChangeReason.Clear: + { + source.ClearOrRemoveMany(item); + break; + } + + case ListChangeReason.Replace: + { + var change = item.Item; + if (change.CurrentIndex >= 0 && change.CurrentIndex == change.PreviousIndex) + { + source[change.CurrentIndex] = change.Current; + } + else + { + if (change.PreviousIndex == -1) + { + source.Remove(change.Previous.Value); + } + else + { + // is this best? or replace + move? + source.RemoveAt(change.PreviousIndex); + } + + if (change.CurrentIndex == -1) + { + source.Add(change.Current); + } + else + { + source.Insert(change.CurrentIndex, change.Current); + } + } + + break; + } + + case ListChangeReason.Refresh: + { + if (changeAware is not null) + { + changeAware.RefreshAt(item.Item.CurrentIndex); + } + else + { + source.RemoveAt(item.Item.CurrentIndex); + source.Insert(item.Item.CurrentIndex, item.Item.Current); + } + + break; + } + + case ListChangeReason.Remove: + { + var change = item.Item; + bool hasIndex = change.CurrentIndex >= 0; + if (hasIndex) + { + source.RemoveAt(change.CurrentIndex); + } + else + { + int index = source.IndexOf(change.Current, equalityComparer); + if (index > -1) + { + source.RemoveAt(index); + } + } + + break; + } + + case ListChangeReason.RemoveRange: + { + // ignore this case because WhereReasonsAre removes the index [in which case call RemoveMany] + //// if (item.Range.Index < 0) + //// throw new UnspecifiedIndexException("ListChangeReason.RemoveRange should not have an index specified index"); + if (item.Range.Index >= 0 && (source is IExtendedList || source is List)) + { + source.RemoveRange(item.Range.Index, item.Range.Count); + } + else + { + source.RemoveMany(item.Range); + } + + break; + } + + case ListChangeReason.Moved: + { + var change = item.Item; + bool hasIndex = change.CurrentIndex >= 0; + if (!hasIndex) + { + throw new UnspecifiedIndexException("Cannot move as an index was not specified"); + } + + if (source is IExtendedList extendedList) + { + extendedList.Move(change.PreviousIndex, change.CurrentIndex); + } + else if (source is ObservableCollection observableCollection) + { + observableCollection.Move(change.PreviousIndex, change.CurrentIndex); + } + else + { + // check this works whatever the index is + source.RemoveAt(change.PreviousIndex); + source.Insert(change.CurrentIndex, change.Current); + } + + break; + } } + } - var index = source.IndexOf(original); - if (index == -1) + /// + /// Removes the number of items, starting at the specified index. + /// + /// The type of the item. + /// The source. + /// The index. + /// The count. + /// Cannot remove range. + private static void RemoveRange(this IList source, int index, int count) + { + if (source is null) { - throw new ArgumentException("Cannot find index of original item. Either it does not exist in the list or the hashcode has mutated"); + throw new ArgumentNullException(nameof(source)); } - if (comparer.Equals(source[index], replaceWith)) + switch (source) { - source[index] = replaceWith; + case List list: + list.RemoveRange(index, count); + break; + case IExtendedList list: + list.RemoveRange(index, count); + break; + default: + throw new NotSupportedException($"Cannot remove range from {source.GetType().FullName}"); } } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/ListFilterPolicy.cs b/src/DynamicData/List/ListFilterPolicy.cs index 3604c25c6..2c847ee3c 100644 --- a/src/DynamicData/List/ListFilterPolicy.cs +++ b/src/DynamicData/List/ListFilterPolicy.cs @@ -1,8 +1,11 @@ -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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 { /// - /// Specifies which filter strategy should be used when the filter predicate is changed + /// Specifies which filter strategy should be used when the filter predicate is changed. /// public enum ListFilterPolicy { diff --git a/src/DynamicData/List/ObservableListEx.cs b/src/DynamicData/List/ObservableListEx.cs index f06da81bf..408113525 100644 --- a/src/DynamicData/List/ObservableListEx.cs +++ b/src/DynamicData/List/ObservableListEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -13,7 +13,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; -using DynamicData.Annotations; + using DynamicData.Binding; using DynamicData.Cache.Internal; using DynamicData.Kernel; @@ -21,292 +21,259 @@ using DynamicData.List.Linq; // ReSharper disable once CheckNamespace - namespace DynamicData { /// - /// Extensions for ObservableList + /// Extensions for ObservableList. /// public static class ObservableListEx { - #region Populate change set from standard rx observable - /// - /// Converts the observable to an observable changeset. - /// Change set observes observable change events. + /// Injects a side effect into a change set observable. /// - /// The type of the object. + /// The type of the item. /// The source. - /// The scheduler (only used for time expiry). - /// - /// source + /// The adaptor. + /// An observable which emits the change set. + /// + /// source /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable source, - IScheduler scheduler = null) + /// adaptor. + /// + public static IObservable> Adapt(this IObservable> source, IChangeSetAdaptor adaptor) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return ToObservableChangeSet(source, null, -1, scheduler); + if (adaptor is null) + { + throw new ArgumentNullException(nameof(adaptor)); + } + + return Observable.Create>( + observer => + { + var locker = new object(); + return source.Synchronize(locker).Select( + changes => + { + adaptor.Adapt(changes); + return changes; + }).SubscribeSafe(observer); + }); } /// - /// Converts the observable to an observable changeset, allowing time expiry to be specified. - /// Change set observes observable change events. + /// Adds a key to the change set result which enables all observable cache features of dynamic data. /// - /// The type of the object. + /// + /// All indexed changes are dropped i.e. sorting is not supported by this function. + /// + /// The type of object. + /// The type of key. /// The source. - /// Specify on a per object level the maximum time before an object expires from a cache - /// The scheduler (only used for time expiry). - /// - /// source - /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable source, - Func expireAfter, - IScheduler scheduler = null) + /// The key selector. + /// An observable which emits the change set. + public static IObservable> AddKey(this IObservable> source, Func keySelector) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (expireAfter == null) + if (keySelector is null) { - throw new ArgumentNullException(nameof(expireAfter)); + throw new ArgumentNullException(nameof(keySelector)); } - return ToObservableChangeSet(source, expireAfter, -1, scheduler); + return source.Select(changes => new ChangeSet(new AddKeyEnumerator(changes, keySelector))); } /// - /// Converts the observable to an observable changeset, with a specified limit of how large the list can be. - /// Change set observes observable change events. + /// Apply a logical And operator between the collections. + /// Items which are in all of the sources are included in the result. /// - /// The type of the object. + /// The type of the item. /// The source. - /// Remove the oldest items when the size has reached this limit - /// The scheduler (only used for time expiry). - /// - /// source - /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable source, - int limitSizeTo, - IScheduler scheduler = null) + /// The others. + /// An observable which emits the change set. + public static IObservable> And(this IObservable> source, params IObservable>[] others) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return ToObservableChangeSet(source, null, limitSizeTo, scheduler); + return source.Combine(CombineOperator.And, others); } /// - /// Converts the observable to an observable changeset, allowing size and time limit to be specified. - /// Change set observes observable change events. + /// Apply a logical And operator between the collections. + /// Items which are in all of the sources are included in the result. /// - /// The type of the object. - /// The source. - /// Specify on a per object level the maximum time before an object expires from a cache - /// Remove the oldest items when the size has reached this limit - /// The scheduler (only used for time expiry). - /// - /// source - /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable source, - Func expireAfter, - int limitSizeTo, - IScheduler scheduler = null) + /// The type of the item. + /// The sources. + /// An observable which emits the change set. + public static IObservable> And(this ICollection>> sources) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return new ToObservableChangeSet(source, expireAfter, limitSizeTo, scheduler).Run(); + return sources.Combine(CombineOperator.And); } /// - /// Converts the observable to an observable changeset. - /// Change set observes observable change events. + /// Dynamically apply a logical And operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// - /// The type of the object. - /// The source. - /// The scheduler (only used for time expiry). - /// - /// source - /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable> source, - IScheduler scheduler = null) + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> And(this IObservableList>> sources) { - return ToObservableChangeSet(source, null, -1, scheduler); + return sources.Combine(CombineOperator.And); } /// - /// Converts the observable to an observable changeset, allowing size and time limit to be specified. - /// Change set observes observable change events. + /// Dynamically apply a logical And operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// - /// The type of the object. - /// The source. - /// Remove the oldest items when the size has reached this limit - /// The scheduler (only used for time expiry). - /// - /// source - /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable> source, - int limitSizeTo, - IScheduler scheduler = null) + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> And(this IObservableList> sources) { - return ToObservableChangeSet(source, null, limitSizeTo, scheduler); + return sources.Combine(CombineOperator.And); } /// - /// Converts the observable to an observable changeset, allowing size to be specified. - /// Change set observes observable change events. + /// Dynamically apply a logical And operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// - /// The type of the object. - /// The source. - /// Specify on a per object level the maximum time before an object expires from a cache - /// The scheduler (only used for time expiry). - /// - /// source - /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable> source, - Func expireAfter, - IScheduler scheduler = null) + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> And(this IObservableList> sources) { - return ToObservableChangeSet(source, expireAfter, 0, scheduler); + return sources.Combine(CombineOperator.And); } /// - /// Converts the observable to an observable changeset, allowing size and time limit to be specified. - /// Change set observes observable change events. + /// Converts the source list to an read only observable list. /// - /// The type of the object. + /// The type of the item. /// The source. - /// Specify on a per object level the maximum time before an object expires from a cache - /// Remove the oldest items when the size has reached this limit - /// The scheduler (only used for time expiry). - /// - /// source - /// or - /// keySelector - public static IObservable> ToObservableChangeSet(this IObservable> source, - Func expireAfter, - int limitSizeTo, - IScheduler scheduler = null) + /// An observable list. + /// source. + public static IObservableList AsObservableList(this ISourceList source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return new ToObservableChangeSet(source, expireAfter, limitSizeTo, scheduler).Run(); + return new AnonymousObservableList(source); } - #endregion + /// + /// Converts the source observable to an read only observable list. + /// + /// The type of the item. + /// The source. + /// An observable list. + /// source. + public static IObservableList AsObservableList(this IObservable> source) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - #region Auto Refresh + return new AnonymousObservableList(source); + } /// /// Automatically refresh downstream operators when any property changes. /// - /// The source observable - /// Batch up changes by specifying the buffer. This greatly increases performance when many elements have sucessive property changes - /// When observing on multiple property changes, apply a throttle to prevent excessive refesh invocations - /// The scheduler - /// An observable change set with additional refresh changes - public static IObservable> AutoRefresh(this IObservable> source, - TimeSpan? changeSetBuffer = null, - TimeSpan? propertyChangeThrottle = null, - IScheduler scheduler = null) + /// The type of object. + /// The source observable. + /// Batch up changes by specifying the buffer. This greatly increases performance when many elements have successive property changes. + /// When observing on multiple property changes, apply a throttle to prevent excessive refresh invocations. + /// The scheduler. + /// An observable change set with additional refresh changes. + public static IObservable> AutoRefresh(this IObservable> source, TimeSpan? changeSetBuffer = null, TimeSpan? propertyChangeThrottle = null, IScheduler? scheduler = null) where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.AutoRefreshOnObservable(t => - { - if (propertyChangeThrottle == null) - { - return t.WhenAnyPropertyChanged(); - } - - return t.WhenAnyPropertyChanged() - .Throttle(propertyChangeThrottle.Value, scheduler ?? Scheduler.Default); + return source.AutoRefreshOnObservable( + t => + { + if (propertyChangeThrottle is null) + { + return t.WhenAnyPropertyChanged(); + } - }, changeSetBuffer, scheduler); + return t.WhenAnyPropertyChanged().Throttle(propertyChangeThrottle.Value, scheduler ?? Scheduler.Default); + }, + changeSetBuffer, + scheduler); } /// /// Automatically refresh downstream operators when properties change. /// - /// The source observable - /// Specify a property to observe changes. When it changes a Refresh is invoked - /// Batch up changes by specifying the buffer. This greatly increases performance when many elements have sucessive property changes - /// When observing on multiple property changes, apply a throttle to prevent excessive refesh invocations - /// The scheduler - /// An observable change set with additional refresh changes - public static IObservable> AutoRefresh(this IObservable> source, - Expression> propertyAccessor, - TimeSpan? changeSetBuffer = null, - TimeSpan? propertyChangeThrottle = null, - IScheduler scheduler = null) - where TObject : INotifyPropertyChanged - { - if (source == null) + /// The type of object. + /// The type of property. + /// The source observable. + /// Specify a property to observe changes. When it changes a Refresh is invoked. + /// Batch up changes by specifying the buffer. This greatly increases performance when many elements have successive property changes. + /// When observing on multiple property changes, apply a throttle to prevent excessive refresh invocations. + /// The scheduler. + /// An observable change set with additional refresh changes. + public static IObservable> AutoRefresh(this IObservable> source, Expression> propertyAccessor, TimeSpan? changeSetBuffer = null, TimeSpan? propertyChangeThrottle = null, IScheduler? scheduler = null) + where TObject : INotifyPropertyChanged + { + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertyAccessor == null) + if (propertyAccessor is null) { throw new ArgumentNullException(nameof(propertyAccessor)); } - return source.AutoRefreshOnObservable(t => - { - if (propertyChangeThrottle == null) - { - return t.WhenPropertyChanged(propertyAccessor, false); - } - - return t.WhenPropertyChanged(propertyAccessor,false) - .Throttle(propertyChangeThrottle.Value, scheduler ?? Scheduler.Default); + return source.AutoRefreshOnObservable( + t => + { + if (propertyChangeThrottle is null) + { + return t.WhenPropertyChanged(propertyAccessor, false); + } - }, changeSetBuffer, scheduler); + return t.WhenPropertyChanged(propertyAccessor, false).Throttle(propertyChangeThrottle.Value, scheduler ?? Scheduler.Default); + }, + changeSetBuffer, + scheduler); } /// - /// Automatically refresh downstream operator. The refresh is triggered when the observable receives a notification + /// Automatically refresh downstream operator. The refresh is triggered when the observable receives a notification. /// - /// The source observable change set - /// An observable which acts on items within the collection and produces a value when the item should be refreshed - /// Batch up changes by specifying the buffer. This greatly increases performance when many elements require a refresh - /// The scheduler - /// An observable change set with additional refresh changes - public static IObservable> AutoRefreshOnObservable(this IObservable> source, - Func> reevaluator, - TimeSpan? changeSetBuffer = null, - IScheduler scheduler = null) + /// The type of object. + /// A ignored type used for specifying what to auto refresh on. + /// The source observable change set. + /// An observable which acts on items within the collection and produces a value when the item should be refreshed. + /// Batch up changes by specifying the buffer. This greatly increases performance when many elements require a refresh. + /// The scheduler. + /// An observable change set with additional refresh changes. + public static IObservable> AutoRefreshOnObservable(this IObservable> source, Func> reevaluator, TimeSpan? changeSetBuffer = null, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (reevaluator == null) + if (reevaluator is null) { throw new ArgumentNullException(nameof(reevaluator)); } @@ -315,150 +282,224 @@ public static IObservable> AutoRefreshOnObservable - /// Supress refresh notifications + /// Binds a clone of the observable change set to the target observable collection. /// - /// The source observable change set - /// - public static IObservable> SupressRefresh(this IObservable> source) + /// The type of the item. + /// The source. + /// The target collection. + /// The reset threshold. + /// An observable which emits the change set. + /// + /// source + /// or + /// targetCollection. + /// + public static IObservable> Bind(this IObservable> source, IObservableCollection targetCollection, int resetThreshold = 25) { - return source.WhereReasonsAreNot(ListChangeReason.Refresh); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - #endregion + if (targetCollection is null) + { + throw new ArgumentNullException(nameof(targetCollection)); + } - #region Conversion + var adaptor = new ObservableCollectionAdaptor(targetCollection, resetThreshold); + return source.Adapt(adaptor); + } /// - /// Removes the index from all changes. - /// - /// NB: This operator has been introduced as a temporary fix for creating an Or operator using merge many. + /// Creates a binding to a readonly observable collection which is specified as an 'out' parameter. /// - /// The type of the object. + /// The type of the item. /// The source. - /// - /// - public static IObservable> RemoveIndex([NotNull] this IObservable> source) + /// 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) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Select(changes => new ChangeSet(changes.YieldWithoutIndex())); + var target = new ObservableCollectionExtended(); + var result = new ReadOnlyObservableCollection(target); + var adaptor = new ObservableCollectionAdaptor(target, resetThreshold); + readOnlyObservableCollection = result; + return source.Adapt(adaptor); } +#if SUPPORTS_BINDINGLIST /// - /// Adds a key to the change set result which enables all observable cache features of dynamic data + /// Binds a clone of the observable change set to the target observable collection. /// - /// - /// All indexed changes are dropped i.e. sorting is not supported by this function - /// - /// The type of object. - /// The type of key. + /// The type of the item. /// The source. - /// The key selector. - /// + /// The target binding list. + /// The reset threshold. /// + /// source + /// or + /// targetCollection. /// - public static IObservable> AddKey( - [NotNull] this IObservable> source, [NotNull] Func keySelector) + /// An observable which emits the change set. + public static IObservable> Bind(this IObservable> source, BindingList bindingList, int resetThreshold = 25) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (keySelector == null) + if (bindingList is null) { - throw new ArgumentNullException(nameof(keySelector)); + throw new ArgumentNullException(nameof(bindingList)); } - return source.Select(changes => new ChangeSet(new AddKeyEnumerator(changes, keySelector))); + return source.Adapt(new BindingListAdaptor(bindingList, resetThreshold)); + } + +#endif + + /// + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. + /// + /// The type of the object. + /// The source. + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// The scheduler. + /// An observable which emits the change set. + /// source. + public static IObservable> BufferIf(this IObservable> source, IObservable pauseIfTrueSelector, IScheduler? scheduler = null) + { + return BufferIf(source, pauseIfTrueSelector, false, scheduler); } /// - /// Convert the object using the sepcified conversion function. - /// - /// This is a lighter equivalent of Transform and is designed to be used with non-disposable objects + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. /// - /// The type of the object. - /// The type of the destination. + /// The type of the object. /// The source. - /// The conversion factory. - /// - /// - /// - [Obsolete("Prefer Cast as it is does the same thing but is semantically correct")] - public static IObservable> Convert( - [NotNull] this IObservable> source, - [NotNull] Func conversionFactory) + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// if set to true [initial pause state]. + /// The scheduler. + /// An observable which emits the change set. + /// source. + public static IObservable> BufferIf(this IObservable> source, IObservable pauseIfTrueSelector, bool initialPauseState, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (conversionFactory == null) + if (pauseIfTrueSelector is null) { - throw new ArgumentNullException(nameof(conversionFactory)); + throw new ArgumentNullException(nameof(pauseIfTrueSelector)); } - return source.Select(changes => changes.Transform(conversionFactory)); + return BufferIf(source, pauseIfTrueSelector, initialPauseState, null, scheduler); + } + + /// + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. + /// + /// The type of the object. + /// The source. + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// Specify a time to ensure the buffer window does not stay open for too long. + /// The scheduler. + /// An observable which emits the change set. + /// source. + public static IObservable> BufferIf(this IObservable> source, IObservable pauseIfTrueSelector, TimeSpan? timeOut, IScheduler? scheduler = null) + { + return BufferIf(source, pauseIfTrueSelector, false, timeOut, scheduler); } /// - /// Cast the underlying type of an object. Use before a Cast function + /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. + /// When a resume signal has been received the batched updates will be fired. /// - /// + /// The type of the object. /// The source. - /// - public static IObservable> CastToObject(this IObservable> source) + /// When true, observable begins to buffer and when false, window closes and buffered result if notified. + /// if set to true [initial pause state]. + /// Specify a time to ensure the buffer window does not stay open for too long. + /// The scheduler. + /// An observable which emits the change set. + /// source. + public static IObservable> BufferIf(this IObservable> source, IObservable pauseIfTrueSelector, bool initialPauseState, TimeSpan? timeOut, IScheduler? scheduler = null) { - return source.Select(changes => + if (source is null) { - return changes.Transform(t => (object)t); - }); + throw new ArgumentNullException(nameof(source)); + } + + if (pauseIfTrueSelector is null) + { + throw new ArgumentNullException(nameof(pauseIfTrueSelector)); + } + + return new BufferIf(source, pauseIfTrueSelector, initialPauseState, timeOut, scheduler).Run(); } /// - /// Cast the changes to another form + /// Buffers changes for an initial period only. After the period has elapsed, not further buffering occurs. + /// + /// The type of object. + /// The source change set. + /// The period to buffer, measure from the time that the first item arrives. + /// The scheduler to buffer on. + /// An observable which emits the change set. + public static IObservable> BufferInitial(this IObservable> source, TimeSpan initialBuffer, IScheduler? scheduler = null) + { + return source.DeferUntilLoaded().Publish( + shared => + { + var initial = shared.Buffer(initialBuffer, scheduler ?? Scheduler.Default).FlattenBufferResult().Take(1); + + return initial.Concat(shared); + }); + } + + /// + /// Cast the changes to another form. /// /// The type of the destination. /// The source. - /// - /// - /// - public static IObservable> Cast([NotNull] this IObservable> source) + /// An observable which emits the change set. + public static IObservable> Cast(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Select(changes => changes.Transform(t=>(TDestination)t)); + return source.Select(changes => changes.Transform(t => (TDestination)t)); } /// /// Cast the changes to another form /// - /// Alas, I had to add the converter due to type inference issues. The converter can be avoided by CastToObject() first + /// Alas, I had to add the converter due to type inference issues. The converter can be avoided by CastToObject() first. /// /// The type of the object. /// The type of the destination. /// The source. /// The conversion factory. - /// - /// - /// - public static IObservable> Cast([NotNull] this IObservable> source, - [NotNull] Func conversionFactory) + /// An observable which emits the change set. + public static IObservable> Cast(this IObservable> source, Func conversionFactory) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (conversionFactory == null) + if (conversionFactory is null) { throw new ArgumentNullException(nameof(conversionFactory)); } @@ -466,241 +507,256 @@ public static IObservable> Cast( return source.Select(changes => changes.Transform(conversionFactory)); } - #endregion + /// + /// Cast the underlying type of an object. Use before a Cast function. + /// + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> CastToObject(this IObservable> source) + { + return source.Select(changes => changes.Transform(t => (object?)t)); + } + + /// + /// Clones the target list as a side effect of the stream. + /// + /// The type of the item. + /// The source. + /// The target of the clone. + /// An observable which emits the change set. + /// source. + public static IObservable> Clone(this IObservable> source, IList target) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - #region Binding + return source.Do(target.Clone); + } /// - /// Binds a clone of the observable changeset to the target observable collection + /// Convert the object using the specified conversion function. + /// + /// This is a lighter equivalent of Transform and is designed to be used with non-disposable objects. /// - /// + /// The type of the object. + /// The type of the destination. /// The source. - /// The target collection. - /// The reset threshold. - /// - /// - /// source - /// or - /// targetCollection - /// - public static IObservable> Bind([NotNull] this IObservable> source, - [NotNull] IObservableCollection targetCollection, int resetThreshold = 25) + /// The conversion factory. + /// An observable which emits the change set. + [Obsolete("Prefer Cast as it is does the same thing but is semantically correct")] + public static IObservable> Convert(this IObservable> source, Func conversionFactory) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (targetCollection == null) + if (conversionFactory is null) { - throw new ArgumentNullException(nameof(targetCollection)); + throw new ArgumentNullException(nameof(conversionFactory)); } - var adaptor = new ObservableCollectionAdaptor(targetCollection, resetThreshold); - return source.Adapt(adaptor); + return source.Select(changes => changes.Transform(conversionFactory)); } /// - /// Creates a binding to a readonly observable collection which is specified as an 'out' parameter + /// Defer the subscription until the stream has been inflated with data. /// - /// + /// The type of the object. /// The source. - /// The resulting read only observable collection. - /// The reset threshold. - /// A continuation of the source stream - /// - /// - public static IObservable> Bind([NotNull] this IObservable> source, - out ReadOnlyObservableCollection readOnlyObservableCollection, int resetThreshold = 25) + /// An observable which emits the change set. + public static IObservable> DeferUntilLoaded(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - var target = new ObservableCollectionExtended(); - var result = new ReadOnlyObservableCollection(target); - var adaptor = new ObservableCollectionAdaptor(target, resetThreshold); - readOnlyObservableCollection = result; - return source.Adapt(adaptor); + return new DeferUntilLoaded(source).Run(); } -#if SUPPORTS_BINDINGLIST - /// - /// Binds a clone of the observable changeset to the target observable collection + /// Defer the subscription until the cache has been inflated with data. /// - /// + /// The type of the object. /// The source. - /// The target binding list - /// The reset threshold. - /// - /// source - /// or - /// targetCollection - /// - public static IObservable> Bind([NotNull] this IObservable> source, - [NotNull] BindingList bindingList, int resetThreshold = 25) + /// An observable which emits the change set. + public static IObservable> DeferUntilLoaded(this IObservableList source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (bindingList == null) - { - throw new ArgumentNullException(nameof(bindingList)); - } - - return source.Adapt(new BindingListAdaptor(bindingList, resetThreshold)); + return source.Connect().DeferUntilLoaded(); } -#endif + /// + /// Disposes each item when no longer required. + /// + /// Individual items are disposed when removed or replaced. All items + /// are disposed when the stream is disposed. + /// + /// The type of the object. + /// The source. + /// A continuation of the original stream. + /// source. + public static IObservable> DisposeMany(this IObservable> source) + { + return source.OnItemRemoved( + t => + { + var d = t as IDisposable; + d?.Dispose(); + }); + } /// - /// Injects a side effect into a changeset observable + /// Selects distinct values from the source, using the specified value selector. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// The adaptor. - /// + /// The transform factory. + /// An observable which emits the change set. /// /// source /// or - /// adaptor + /// valueSelector. /// - public static IObservable> Adapt([NotNull] this IObservable> source, - [NotNull] IChangeSetAdaptor adaptor) + public static IObservable> DistinctValues(this IObservable> source, Func valueSelector) + where TValue : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (adaptor == null) + if (valueSelector is null) { - throw new ArgumentNullException(nameof(adaptor)); + throw new ArgumentNullException(nameof(valueSelector)); } - return Observable.Create>(observer => - { - var locker = new object(); - return source - .Synchronize(locker) - .Select(changes => - { - adaptor.Adapt(changes); - return changes; - }).SubscribeSafe(observer); - }); + return new Distinct(source, valueSelector).Run(); } - #endregion - - #region Populate into an observable list - /// - /// list. + /// Apply a logical Except operator between the collections. + /// Items which are in the source and not in the others are included in the result. /// - /// The type of the object. + /// The type of the item. /// The source. - /// The destination. - /// - /// - /// source - /// or - /// destination - /// - /// source - /// or - /// destination - public static IDisposable PopulateInto([NotNull] this IObservable> source, - [NotNull] ISourceList destination) + /// The others. + /// An observable which emits the change set. + public static IObservable> Except(this IObservable> source, params IObservable>[] others) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + return source.Combine(CombineOperator.Except, others); + } - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } + /// + /// Apply a logical Except operator between the collections. + /// Items which are in the source and not in the others are included in the result. + /// + /// The type of the item. + /// The sources. + /// An observable which emits the change set. + public static IObservable> Except(this ICollection>> sources) + { + return sources.Combine(CombineOperator.Except); + } - return source.Subscribe(changes => destination.Edit(updater => updater.Clone(changes))); + /// + /// Dynamically apply a logical Except operator. Items from the first observable list are included when an equivalent item does not exist in the other sources. + /// + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> Except(this IObservableList>> sources) + { + return sources.Combine(CombineOperator.Except); } /// - /// Converts the source list to an read only observable list + /// Dynamically apply a logical Except operator. Items from the first observable list are included when an equivalent item does not exist in the other sources. /// - /// - /// The source. - /// - /// source - public static IObservableList AsObservableList([NotNull] this ISourceList source) + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> Except(this IObservableList> sources) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + return sources.Combine(CombineOperator.Except); + } - return new AnonymousObservableList(source); + /// + /// Dynamically apply a logical Except operator. Items from the first observable list are included when an equivalent item does not exist in the other sources. + /// + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> Except(this IObservableList> sources) + { + return sources.Combine(CombineOperator.Except); } /// - /// Converts the source observable to an read only observable list + /// Removes items from the cache according to the value specified by the time selector function. /// - /// + /// The type of the item. /// The source. - /// - /// source - public static IObservableList AsObservableList([NotNull] this IObservable> source) + /// Selector returning when to expire the item. Return null for non-expiring item. + /// The scheduler. + /// An observable which emits the enumerable of items. + public static IObservable> ExpireAfter(this ISourceList source, Func timeSelector, IScheduler? scheduler = null) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return new AnonymousObservableList(source); + return source.ExpireAfter(timeSelector, null, scheduler); } /// - /// List equivalent to Publish().RefCount(). The source is cached so long as there is at least 1 subscriber. + /// Removes items from the cache according to the value specified by the time selector function. /// - /// + /// The type of the item. /// The source. - /// - /// - public static IObservable> RefCount([NotNull] this IObservable> source) + /// Selector returning when to expire the item. Return null for non-expiring item. + /// Enter the polling interval to optimise expiry timers, if omitted 1 timer is created for each unique expiry time. + /// The scheduler. + /// An observable which emits the enumerable of items. + public static IObservable> ExpireAfter(this ISourceList source, Func timeSelector, TimeSpan? pollingInterval = null, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return new RefCount(source).Run(); - } + if (timeSelector is null) + { + throw new ArgumentNullException(nameof(timeSelector)); + } - #endregion + var locker = new object(); + var limiter = new ExpireAfter(source, timeSelector, pollingInterval, scheduler ?? Scheduler.Default, locker); - #region Core List Operators + return limiter.Run().Synchronize(locker).Do(source.RemoveMany); + } /// - /// Filters the source using the specified valueSelector + /// Filters the source using the specified valueSelector. /// - /// + /// The type of the item. /// The source. /// The valueSelector. - /// - /// source + /// An observable which emits the change set. + /// source. public static IObservable> Filter(this IObservable> source, Func predicate) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (predicate == null) + if (predicate is null) { throw new ArgumentNullException(nameof(predicate)); } @@ -711,22 +767,22 @@ public static IObservable> Filter(this IObservable /// Filters source using the specified filter observable predicate. /// - /// + /// The type of the item. /// The source. - /// - /// Should the filter clear and replace, or calculate a diff-set - /// + /// The predicate which indicates which items should be included. + /// Should the filter clear and replace, or calculate a diff-set. + /// An observable which emits the change set. /// source /// or - /// filterController - public static IObservable> Filter([NotNull] this IObservable> source, [NotNull] IObservable> predicate, ListFilterPolicy filterPolicy = ListFilterPolicy.CalculateDiff) + /// filterController. + public static IObservable> Filter(this IObservable> source, IObservable> predicate, ListFilterPolicy filterPolicy = ListFilterPolicy.CalculateDiff) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (predicate == null) + if (predicate is null) { throw new ArgumentNullException(nameof(predicate)); } @@ -734,39 +790,55 @@ public static IObservable> Filter([NotNull] this IObservable(source, predicate, filterPolicy).Run(); } + /// + /// Filters source on the specified observable property using the specified predicate. + /// + /// The filter will automatically reapply when a property changes. + /// + /// The type of the object. + /// The source. + /// The filter property selector. When the observable changes the filter will be re-evaluated. + /// The property changed throttle. + /// The scheduler used when throttling. + /// An observable which emits the change set. + public static IObservable> FilterOnObservable(this IObservable> source, Func> objectFilterObservable, TimeSpan? propertyChangedThrottle = null, IScheduler? scheduler = null) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return new FilterOnObservable(source, objectFilterObservable, propertyChangedThrottle, scheduler).Run(); + } + /// /// Filters source on the specified property using the specified predicate. /// - /// The filter will automatically reapply when a property changes + /// The filter will automatically reapply when a property changes. /// /// The type of the object. /// The type of the property. /// The source. - /// The property selector. When the property changes the filter specified will be re-evaluated - /// A predicate based on the object which contains the changed property + /// The property selector. When the property changes the filter specified will be re-evaluated. + /// A predicate based on the object which contains the changed property. /// The property changed throttle. - /// The scheduler used when throttling - /// - /// - /// + /// The scheduler used when throttling. + /// An observable which emits the change set. [Obsolete("Use AutoRefresh(), followed by Filter() instead")] - public static IObservable> FilterOnProperty(this IObservable> source, - Expression> propertySelector, - Func predicate, - TimeSpan? propertyChangedThrottle = null, - IScheduler scheduler = null) where TObject : INotifyPropertyChanged + public static IObservable> FilterOnProperty(this IObservable> source, Expression> propertySelector, Func predicate, TimeSpan? propertyChangedThrottle = null, IScheduler? scheduler = null) + where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertySelector == null) + if (propertySelector is null) { throw new ArgumentNullException(nameof(propertySelector)); } - if (predicate == null) + if (predicate is null) { throw new ArgumentNullException(nameof(predicate)); } @@ -775,745 +847,621 @@ public static IObservable> FilterOnProperty - /// Filters source on the specified observable property using the specified predicate. - /// - /// The filter will automatically reapply when a property changes + /// Convert the result of a buffer operation to a change set. + /// + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> FlattenBufferResult(this IObservable>> source) + { + return source.Where(x => x.Count != 0).Select(updates => new ChangeSet(updates.SelectMany(u => u))); + } + + /// + /// Provides a call back for each item change. /// /// The type of the object. /// The source. - /// The filter property selector. When the observable changes the filter will be re-evaluated - /// The property changed throttle. - /// The scheduler used when throttling - /// - /// - /// - public static IObservable> FilterOnObservable(this IObservable> source, - Func> objectFilterObservable, - TimeSpan? propertyChangedThrottle = null, - IScheduler scheduler = null) + /// The action. + /// An observable which emits the change set. + public static IObservable> ForEachChange(this IObservable> source, Action> action) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return new FilterOnObservable(source, objectFilterObservable, propertyChangedThrottle, scheduler).Run(); + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return source.Do(changes => changes.ForEach(action)); } /// - /// Reverse sort of the changset + /// Provides a call back for each item change. + /// + /// Range changes are flattened, so there is only need to check for Add, Replace, Remove and Clear. /// - /// + /// The type of the object. /// The source. - /// - /// - /// source - /// or - /// comparer - /// - public static IObservable> Reverse(this IObservable> source) + /// The action. + /// An observable which emits the change set. + public static IObservable> ForEachItemChange(this IObservable> source, Action> action) { - var reverser = new Reverser(); - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Select(changes => new ChangeSet(reverser.Reverse(changes))); + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return source.Do(changes => changes.Flatten().ForEach(action)); } /// - /// Projects each update item to a new form using the specified transform function + /// Groups the source on the value returned by group selector factory. The groupings contains an inner observable list. /// - /// The type of the source. - /// The type of the destination. + /// The type of the object. + /// The type of the group. /// The source. - /// The transform factory. - /// Should a new transform be applied when a refresh event is received - /// + /// The group selector. + /// Force the grouping function to recalculate the group value. + /// For example if you have a time based grouping with values like `Last Minute', 'Last Hour', 'Today' etc regrouper is used to refresh these groupings. + /// An observable which emits the change set. /// /// source /// or - /// valueSelector + /// groupSelector. /// - public static IObservable> Transform(this IObservable> source, - Func transformFactory, - bool transformOnRefresh = false) + public static IObservable>> GroupOn(this IObservable> source, Func groupSelector, IObservable? regrouper = null) + where TGroup : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (groupSelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(groupSelector)); } - return source.Transform((t, previous, idx) => transformFactory(t), transformOnRefresh); + return new GroupOn(source, groupSelector, regrouper).Run(); } /// - /// Projects each update item to a new form using the specified transform function + /// Groups the source using the property specified by the property selector. The resulting groupings contains an inner observable list. + /// Groups are re-applied when the property value changed. + /// When there are likely to be a large number of group property changes specify a throttle to improve performance. /// - /// The type of the source. - /// The type of the destination. + /// The type of the object. + /// The type of the group. /// The source. - /// The transform function - /// Should a new transform be applied when a refresh event is received - /// A an observable changeset of the transformed object - /// - /// source - /// or - /// valueSelector - /// - public static IObservable> Transform(this IObservable> source, - Func transformFactory, - bool transformOnRefresh = false) + /// The property selector used to group the items. + /// The property changed throttle. + /// The scheduler. + /// An observable which emits the change set. + public static IObservable>> GroupOnProperty(this IObservable> source, Expression> propertySelector, TimeSpan? propertyChangedThrottle = null, IScheduler? scheduler = null) + where TGroup : notnull + where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (propertySelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(propertySelector)); } - return source.Transform((t, previous, idx) => transformFactory(t,idx),transformOnRefresh); + return new GroupOnProperty(source, propertySelector, propertyChangedThrottle, scheduler).Run(); } /// - /// Projects each update item to a new form using the specified transform function. - /// - /// *** Annoyingly when using this overload you will have to explicitly specify the generic type arguments as type inference fails + /// Groups the source using the property specified by the property selector. The resulting groupings are immutable. + /// Groups are re-applied when the property value changed. + /// When there are likely to be a large number of group property changes specify a throttle to improve performance. /// - /// The type of the source. - /// The type of the destination. + /// The type of the object. + /// The type of the group. /// The source. - /// The transform function - /// Should a new transform be applied when a refresh event is received - /// A an observable changeset of the transformed object - /// - /// source - /// or - /// valueSelector - /// - public static IObservable> Transform(this IObservable> source, - Func, TDestination> transformFactory, - bool transformOnRefresh = false) + /// The property selector used to group the items. + /// The property changed throttle. + /// The scheduler. + /// An observable which emits the change set. + public static IObservable>> GroupOnPropertyWithImmutableState(this IObservable> source, Expression> propertySelector, TimeSpan? propertyChangedThrottle = null, IScheduler? scheduler = null) + where TGroup : notnull + where TObject : INotifyPropertyChanged { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (propertySelector is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(propertySelector)); } - return source.Transform((t, previous, idx) => transformFactory(t, previous), transformOnRefresh); + return new GroupOnPropertyWithImmutableState(source, propertySelector, propertyChangedThrottle, scheduler).Run(); } /// - /// Projects each update item to a new form using the specified transform function - /// - /// *** Annoyingly when using this overload you will have to explicy specify the generic type arguments as type inference fails + /// Groups the source on the value returned by group selector factory. Each update produces immutable grouping. /// - /// The type of the source. - /// The type of the destination. + /// The type of the object. + /// The type of the group key. /// The source. - /// The transform factory. - /// Should a new transform be applied when a refresh event is received - /// A an observable changeset of the transformed object + /// The group selector key. + /// Force the grouping function to recalculate the group value. + /// For example if you have a time based grouping with values like `Last Minute', 'Last Hour', 'Today' etc regrouper is used to refresh these groupings. + /// An observable which emits the change set. /// /// source /// or - /// valueSelector + /// groupSelectorKey. /// - public static IObservable> Transform(this IObservable> source, - Func, int, TDestination> transformFactory, bool transformOnRefresh = false) + public static IObservable>> GroupWithImmutableState(this IObservable> source, Func groupSelectorKey, IObservable? regrouper = null) + where TGroupKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (groupSelectorKey is null) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentNullException(nameof(groupSelectorKey)); } - return new Transformer(source, transformFactory, transformOnRefresh).Run(); + return new GroupOnImmutable(source, groupSelectorKey, regrouper).Run(); } /// - /// Projects each update item to a new form using the specified transform function + /// Limits the size of the source cache to the specified limit. + /// Notifies which items have been removed from the source list. /// - /// The type of the source. - /// The type of the destination. + /// The type of the item. /// The source. - /// The transform factory. - /// A an observable changeset of the transformed object - /// - /// source - /// or - /// valueSelector - /// - public static IObservable> TransformAsync( - this IObservable> source, Func> transformFactory) + /// The size limit. + /// The scheduler. + /// An observable which emits a enumerable of items. + /// source. + /// sizeLimit cannot be zero. + public static IObservable> LimitSizeTo(this ISourceList source, int sizeLimit, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (sizeLimit <= 0) { - throw new ArgumentNullException(nameof(transformFactory)); + throw new ArgumentException("sizeLimit cannot be zero", nameof(sizeLimit)); } - return new TransformAsync(source, transformFactory).Run(); + var locker = new object(); + var limiter = new LimitSizeTo(source, sizeLimit, scheduler ?? Scheduler.Default, locker); + + return limiter.Run().Synchronize(locker).Do(source.RemoveMany); } /// - /// Equivalent to a select many transform. To work, the key must individually identify each child. + /// Dynamically merges the observable which is selected from each item in the stream, and un-merges the item + /// when it is no longer part of the stream. /// + /// The type of the object. /// The type of the destination. - /// The type of the source. /// The source. - /// The manyselector. - /// Used when an item has been replaced to determine whether child items are the same as previous children - /// - /// - /// source + /// The observable selector. + /// An observable which emits the destination value. + /// source /// or - /// manyselector - /// - public static IObservable> TransformMany( [NotNull] this IObservable> source, - [NotNull] Func> manyselector, - IEqualityComparer equalityComparer = null) + /// observableSelector. + public static IObservable MergeMany(this IObservable> source, Func> observableSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (manyselector == null) + if (observableSelector is null) { - throw new ArgumentNullException(nameof(manyselector)); + throw new ArgumentNullException(nameof(observableSelector)); } - return new TransformMany(source, manyselector, equalityComparer).Run(); + return new MergeMany(source, observableSelector).Run(); } - /// - /// Flatten the nested observable collection, and observe subsequentl observable collection changes + /// + /// Prevents an empty notification. /// - /// The type of the destination. - /// The type of the source. + /// The type of the item. /// The source. - /// The manyselector. - /// Used when an item has been replaced to determine whether child items are the same as previous children - public static IObservable> TransformMany( this IObservable> source, - Func> manyselector, - IEqualityComparer equalityComparer = null) + /// An observable which emits the change set. + /// source. + public static IObservable> NotEmpty(this IObservable> source) { - return new TransformMany(source,manyselector, equalityComparer).Run(); - } - - /// - /// Flatten the nested observable collection, and observe subsequentl observable collection changes - /// - /// The type of the destination. - /// The type of the source. - /// The source. - /// The manyselector. - /// Used when an item has been replaced to determine whether child items are the same as previous children - public static IObservable> TransformMany(this IObservable> source, - Func> manyselector, - IEqualityComparer equalityComparer = null) - { - return new TransformMany(source, manyselector, equalityComparer).Run(); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - /// - /// Flatten the nested observable list, and observe subsequent observable collection changes - /// - /// The type of the destination. - /// The type of the source. - /// The source. - /// The manyselector. - /// Used when an item has been replaced to determine whether child items are the same as previous children - public static IObservable> TransformMany(this IObservable> source, - Func> manyselector, - IEqualityComparer equalityComparer = null) - { - return new TransformMany(source, manyselector, equalityComparer).Run(); + return source.Where(s => s.Count != 0); } /// - /// Selects distinct values from the source, using the specified value selector + /// Callback for each item as and when it is being added to the stream. /// - /// The type of the source. - /// The type of the destination. + /// The type of the item. /// The source. - /// The transform factory. - /// - /// - /// source - /// or - /// valueSelector - /// - public static IObservable> DistinctValues( - this IObservable> source, - Func valueSelector) + /// The add action. + /// An observable which emits the change set. + public static IObservable> OnItemAdded(this IObservable> source, Action addAction) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (valueSelector == null) + if (addAction is null) { - throw new ArgumentNullException(nameof(valueSelector)); + throw new ArgumentNullException(nameof(addAction)); } - return new Distinct(source, valueSelector).Run(); + return new OnBeingAdded(source, addAction).Run(); } /// - /// Groups the source on the value returned by group selector factory. The groupings contains an inner observable list. + /// Callback for each item as and when it is being removed from the stream. /// - /// The type of the object. - /// The type of the group. + /// The type of the object. /// The source. - /// The group selector. - /// Force the grouping function to recalculate the group value. - /// For example if you have a time based grouping with values like `Last Minute', 'Last Hour', 'Today' etc regrouper is used to refresh these groupings - /// + /// The remove action. + /// An observable which emits the change set. /// /// source /// or - /// groupSelector + /// removeAction. /// - public static IObservable>> GroupOn( - this IObservable> source, Func groupSelector, - IObservable regrouper = null) + public static IObservable> OnItemRemoved(this IObservable> source, Action removeAction) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (groupSelector == null) + if (removeAction is null) { - throw new ArgumentNullException(nameof(groupSelector)); + throw new ArgumentNullException(nameof(removeAction)); } - return new GroupOn(source, groupSelector, regrouper).Run(); + return new OnBeingRemoved(source, removeAction).Run(); } /// - /// Groups the source on the value returned by group selector factory. Each update produces immuatable grouping. + /// Apply a logical Or operator between the collections. + /// Items which are in any of the sources are included in the result. /// - /// The type of the object. - /// The type of the group key. + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> Or(this ICollection>> sources) + { + return sources.Combine(CombineOperator.Or); + } + + /// + /// Apply a logical Or operator between the collections. + /// Items which are in any of the sources are included in the result. + /// + /// The type of the item. /// The source. - /// The group selector key. - /// Force the grouping function to recalculate the group value. - /// For example if you have a time based grouping with values like `Last Minute', 'Last Hour', 'Today' etc regrouper is used to refresh these groupings - /// - /// - /// - /// source - /// or - /// groupSelectorKey - /// - public static IObservable>> GroupWithImmutableState - (this IObservable> source, - Func groupSelectorKey, - IObservable regrouper = null) + /// The others. + /// An observable which emits the change set. + public static IObservable> Or(this IObservable> source, params IObservable>[] others) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + return source.Combine(CombineOperator.Or, others); + } - if (groupSelectorKey == null) - { - throw new ArgumentNullException(nameof(groupSelectorKey)); - } + /// + /// Dynamically apply a logical Or operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. + /// + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> Or(this IObservableList>> sources) + { + return sources.Combine(CombineOperator.Or); + } - return new GroupOnImmutable(source, groupSelectorKey, regrouper).Run(); + /// + /// Dynamically apply a logical Or operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. + /// + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> Or(this IObservableList> sources) + { + return sources.Combine(CombineOperator.Or); } /// - /// Groups the source using the property specified by the property selector. The resulting groupings contains an inner observable list. - /// Groups are re-applied when the property value changed. - /// When there are likely to be a large number of group property changes specify a throttle to improve performance + /// Dynamically apply a logical Or operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// - /// The type of the object. - /// The type of the group. + /// The type of the item. + /// The source. + /// An observable which emits the change set. + public static IObservable> Or(this IObservableList> sources) + { + return sources.Combine(CombineOperator.Or); + } + + /// + /// Applies paging to the data source. + /// + /// The type of the item. /// The source. - /// The property selector used to group the items - /// The property changed throttle. - /// The scheduler. - /// - /// - /// - public static IObservable>> GroupOnProperty( - this IObservable> source, - Expression> propertySelector, - TimeSpan? propertyChangedThrottle = null, - IScheduler scheduler = null) - where TObject : INotifyPropertyChanged + /// Observable to control page requests. + /// An observable which emits the change set. + public static IObservable> Page(this IObservable> source, IObservable requests) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertySelector == null) + if (requests is null) { - throw new ArgumentNullException(nameof(propertySelector)); + throw new ArgumentNullException(nameof(requests)); } - return - new GroupOnProperty(source, propertySelector, propertyChangedThrottle, scheduler).Run(); + return new Pager(source, requests).Run(); } /// - /// Groups the source using the property specified by the property selector. The resulting groupings are immutable. - /// Groups are re-applied when the property value changed. - /// When there are likely to be a large number of group property changes specify a throttle to improve performance + /// list. /// - /// The type of the object. - /// The type of the group. + /// The type of the object. /// The source. - /// The property selector used to group the items - /// The property changed throttle. - /// The scheduler. - /// - /// + /// The destination. + /// An observable which emits the change set. + /// + /// source + /// or + /// destination. /// - public static IObservable>> GroupOnPropertyWithImmutableState(this IObservable> source, - Expression> propertySelector, - TimeSpan? propertyChangedThrottle = null, - IScheduler scheduler = null) - where TObject : INotifyPropertyChanged + public static IDisposable PopulateInto(this IObservable> source, ISourceList destination) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertySelector == null) + if (destination is null) { - throw new ArgumentNullException(nameof(propertySelector)); + throw new ArgumentNullException(nameof(destination)); } - return new GroupOnPropertyWithImmutableState(source, propertySelector, propertyChangedThrottle, scheduler).Run(); + return source.Subscribe(changes => destination.Edit(updater => updater.Clone(changes))); } /// - /// Prevents an empty notification + /// The latest copy of the cache is exposed for querying after each modification to the underlying data. /// - /// + /// The type of the object. + /// The type of the destination. /// The source. - /// - /// source - public static IObservable> NotEmpty(this IObservable> source) + /// The result selector. + /// An observable which emits the destination value. + /// + /// source + /// or + /// resultSelector. + /// + public static IObservable QueryWhenChanged(this IObservable> source, Func, TDestination> resultSelector) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.Where(s => s.Count != 0); - } - - /// - /// Clones the target list as a side effect of the stream - /// - /// - /// The source. - /// - /// - /// source - public static IObservable> Clone(this IObservable> source, IList target) - { - if (source == null) + if (resultSelector is null) { - throw new ArgumentNullException(nameof(source)); + throw new ArgumentNullException(nameof(resultSelector)); } - return source.Do(target.Clone); + return source.QueryWhenChanged().Select(resultSelector); } - #endregion - - #region Sort - /// - /// Sorts the sequence using the specified comparer. + /// The latest copy of the cache is exposed for querying i) after each modification to the underlying data ii) upon subscription. /// - /// + /// The type of the object. /// The source. - /// The comparer used for sorting - /// For improved performance, specify SortOptions.UseBinarySearch. This can only be used when the values which are sorted on are immutable - /// Since sorting can be slow for large record sets, the reset threshold is used to force the list re-ordered - /// OnNext of this observable causes data to resort. This is required when the value which is sorted on mutable - /// An observable comparer used to change the comparer on which the sorted list i - /// - /// source - /// or - /// comparer - public static IObservable> Sort(this IObservable> source, - IComparer comparer, - SortOptions options = SortOptions.None, - IObservable resort = null, - IObservable> comparerChanged = null, - int resetThreshold = 50) + /// An observable which emits the read only collection. + /// source. + public static IObservable> QueryWhenChanged(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (comparer == null) - { - throw new ArgumentNullException(nameof(comparer)); - } - - return new Sort(source, comparer, options, resort, comparerChanged, resetThreshold).Run(); + return new QueryWhenChanged(source).Run(); } /// - /// Sorts the sequence using the specified observable comparer. + /// List equivalent to Publish().RefCount(). The source is cached so long as there is at least 1 subscriber. /// - /// + /// The type of the item. /// The source. - /// For improved performance, specify SortOptions.UseBinarySearch. This can only be used when the values which are sorted on are immutable - /// Since sorting can be slow for large record sets, the reset threshold is used to force the list re-ordered - /// OnNext of this observable causes data to resort. This is required when the value which is sorted on mutable - /// An observable comparer used to change the comparer on which the sorted list i - /// - /// source - /// or - /// comparer - public static IObservable> Sort(this IObservable> source, - IObservable> comparerChanged, - SortOptions options = SortOptions.None, - IObservable resort = null, - int resetThreshold = 50) + /// An observable which emits the change set. + public static IObservable> RefCount(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (comparerChanged == null) - { - throw new ArgumentNullException(nameof(comparerChanged)); - } - - return new Sort(source, null, options, resort, comparerChanged, resetThreshold).Run(); + return new RefCount(source).Run(); } - #endregion - - #region Item operators - /// - /// Provides a call back for each item change. + /// Removes the index from all changes. + /// + /// NB: This operator has been introduced as a temporary fix for creating an Or operator using merge many. /// - /// The type of the object. + /// The type of the object. /// The source. - /// The action. - /// - /// - /// - public static IObservable> ForEachChange( - [NotNull] this IObservable> source, - [NotNull] Action> action) + /// An observable which emits the change set. + public static IObservable> RemoveIndex(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - return source.Do(changes => changes.ForEach(action)); + return source.Select(changes => new ChangeSet(changes.YieldWithoutIndex())); } /// - /// Provides a call back for each item change. - /// - /// Range changes are flattened, so there is only need to check for Add, Replace, Remove and Clear + /// Reverse sort of the change set. /// - /// The type of the object. + /// The type of the item. /// The source. - /// The action. - /// + /// An observable which emits the change set. /// + /// source + /// or + /// comparer. /// - public static IObservable> ForEachItemChange( - [NotNull] this IObservable> source, - [NotNull] Action> action) + public static IObservable> Reverse(this IObservable> source) { - if (source == null) + var reverser = new Reverser(); + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - return source.Do(changes => changes.Flatten().ForEach(action)); + return source.Select(changes => new ChangeSet(reverser.Reverse(changes))); } /// - /// Dynamically merges the observable which is selected from each item in the stream, and unmerges the item - /// when it is no longer part of the stream. + /// Defer the subscription until loaded and skip initial change set. /// /// The type of the object. - /// The type of the destination. /// The source. - /// The observable selector. - /// - /// source - /// or - /// observableSelector - public static IObservable MergeMany( - [NotNull] this IObservable> source, - [NotNull] Func> observableSelector) + /// An observable which emits the change set. + /// source. + public static IObservable> SkipInitial(this IObservable> source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (observableSelector == null) - { - throw new ArgumentNullException(nameof(observableSelector)); - } - - return new MergeMany(source, observableSelector).Run(); + return source.DeferUntilLoaded().Skip(1); } /// - /// Watches each item in the collection and notifies when any of them has changed + /// Sorts the sequence using the specified comparer. /// - /// - /// The type of the value. + /// The type of the item. /// The source. - /// The property accessor. - /// if set to true [notify on initial value]. - /// - /// - /// - public static IObservable WhenValueChanged( - [NotNull] this IObservable> source, - [NotNull] Expression> propertyAccessor, - bool notifyOnInitialValue = true) - where TObject : INotifyPropertyChanged + /// The comparer used for sorting. + /// For improved performance, specify SortOptions.UseBinarySearch. This can only be used when the values which are sorted on are immutable. + /// OnNext of this observable causes data to resort. This is required when the value which is sorted on mutable. + /// An observable comparer used to change the comparer on which the sorted list i. + /// Since sorting can be slow for large record sets, the reset threshold is used to force the list re-ordered. + /// An observable which emits the change set. + /// source + /// or + /// comparer. + public static IObservable> Sort(this IObservable> source, IComparer comparer, SortOptions options = SortOptions.None, IObservable? resort = null, IObservable>? comparerChanged = null, int resetThreshold = 50) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertyAccessor == null) + if (comparer is null) { - throw new ArgumentNullException(nameof(propertyAccessor)); + throw new ArgumentNullException(nameof(comparer)); } - var factory = propertyAccessor.GetFactory(); - return source.MergeMany(t => factory(t, notifyOnInitialValue).Select(pv=>pv.Value)); + return new Sort(source, comparer, options, resort, comparerChanged, resetThreshold).Run(); } /// - /// Watches each item in the collection and notifies when any of them has changed + /// Sorts the sequence using the specified observable comparer. /// - /// - /// The type of the value. + /// The type of the item. /// The source. - /// The property accessor. - /// if set to true [notify on initial value]. - /// - /// - /// - public static IObservable> WhenPropertyChanged( - [NotNull] this IObservable> source, - [NotNull] Expression> propertyAccessor, - bool notifyOnInitialValue = true) - where TObject : INotifyPropertyChanged + /// An observable comparer used to change the comparer on which the sorted list i. + /// For improved performance, specify SortOptions.UseBinarySearch. This can only be used when the values which are sorted on are immutable. + /// OnNext of this observable causes data to resort. This is required when the value which is sorted on mutable. + /// Since sorting can be slow for large record sets, the reset threshold is used to force the list re-ordered. + /// An observable which emits the change set. + /// source + /// or + /// comparer. + public static IObservable> Sort(this IObservable> source, IObservable> comparerChanged, SortOptions options = SortOptions.None, IObservable? resort = null, int resetThreshold = 50) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (propertyAccessor == null) + if (comparerChanged is null) { - throw new ArgumentNullException(nameof(propertyAccessor)); + throw new ArgumentNullException(nameof(comparerChanged)); } - var factory = propertyAccessor.GetFactory(); - return source.MergeMany(t => factory(t, notifyOnInitialValue)); + return new Sort(source, null, options, resort, comparerChanged, resetThreshold).Run(); } /// - /// Watches each item in the collection and notifies when any of them has changed + /// Prepends an empty change set to the source. /// - /// The type of the object. - /// The source. - /// specify properties to Monitor, or omit to monitor all property changes - /// - /// - /// - public static IObservable WhenAnyPropertyChanged([NotNull] this IObservable> source, params string[] propertiesToMonitor) - where TObject : INotifyPropertyChanged + /// The type of item. + /// The source observable of change set values. + /// An observable which emits a change set. + public static IObservable> StartWithEmpty(this IObservable> source) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source.MergeMany(t => t.WhenAnyPropertyChanged(propertiesToMonitor)); + return source.StartWith(ChangeSet.Empty); } /// - /// Subscribes to each item when it is added to the stream and unsubcribes when it is removed. All items will be unsubscribed when the stream is disposed + /// Subscribes to each item when it is added to the stream and unsubscribes when it is removed. All items will be unsubscribed when the stream is disposed. /// /// The type of the object. /// The source. - /// The subsription function - /// + /// The subscription function. + /// An observable which emits the change set. /// source /// or - /// subscriptionFactory + /// subscriptionFactory. /// - /// Subscribes to each item when it is added or updates and unsubcribes when it is removed + /// Subscribes to each item when it is added or updates and unsubscribes when it is removed. /// - public static IObservable> SubscribeMany(this IObservable> source, - Func subscriptionFactory) + public static IObservable> SubscribeMany(this IObservable> source, Func subscriptionFactory) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (subscriptionFactory == null) + if (subscriptionFactory is null) { throw new ArgumentNullException(nameof(subscriptionFactory)); } @@ -1522,827 +1470,724 @@ public static IObservable> SubscribeMany(this IObservable - /// Disposes each item when no longer required. - /// - /// Individual items are disposed when removed or replaced. All items - /// are disposed when the stream is disposed + /// Suppress refresh notifications. /// - /// - /// /// The type of the object. - /// The source. - /// A continuation of the original stream - /// source - public static IObservable> DisposeMany(this IObservable> source) + /// The source observable change set. + /// An observable which emits the change set. + public static IObservable> SuppressRefresh(this IObservable> source) { - return source.OnItemRemoved(t => - { - var d = t as IDisposable; - d?.Dispose(); - }); + return source.WhereReasonsAreNot(ListChangeReason.Refresh); } /// - /// Callback for each item as and when it is being removed from the stream + /// Transforms an observable sequence of observable lists into a single sequence + /// producing values only from the most recent observable sequence. + /// Each time a new inner observable sequence is received, unsubscribe from the + /// previous inner observable sequence and clear the existing result set. /// /// The type of the object. - /// The source. - /// The remove action. - /// - /// - /// source - /// or - /// removeAction - /// - public static IObservable> OnItemRemoved(this IObservable> source, - Action removeAction) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (removeAction == null) - { - throw new ArgumentNullException(nameof(removeAction)); - } - - return new OnBeingRemoved(source, removeAction).Run(); - } - - /// - /// Callback for each item as and when it is being added to the stream - /// - /// - /// The source. - /// The add action. - /// - public static IObservable> OnItemAdded(this IObservable> source, - Action addAction) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (addAction == null) - { - throw new ArgumentNullException(nameof(addAction)); - } - - return new OnBeingAdded(source, addAction).Run(); - } - - #endregion - - #region Reason filtering - - /// - /// Includes changes for the specified reasons only - /// - /// - /// The source. - /// The reasons. - /// - /// Must enter at least 1 reason - public static IObservable> WhereReasonsAre(this IObservable> source, - params ListChangeReason[] reasons) + /// The source. + /// + /// The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received. + /// + /// is null. + public static IObservable> Switch(this IObservable> sources) { - if (reasons == null) - { - throw new ArgumentNullException(nameof(reasons)); - } - - if (reasons.Length == 0) + if (sources is null) { - throw new ArgumentException("Must enter at least 1 reason", nameof(reasons)); + throw new ArgumentNullException(nameof(sources)); } - var matches = new HashSet(reasons); - return source.Select(changes => - { - var filtered = changes.Where(change => matches.Contains(change.Reason)).YieldWithoutIndex(); - return new ChangeSet(filtered); - }).NotEmpty(); + return sources.Select(cache => cache.Connect()).Switch(); } /// - /// Excludes updates for the specified reasons + /// Transforms an observable sequence of observable changes sets into an observable sequence + /// producing values only from the most recent observable sequence. + /// Each time a new inner observable sequence is received, unsubscribe from the + /// previous inner observable sequence and clear the existing result set. /// - /// - /// The source. - /// The reasons. - /// - /// Must enter at least 1 reason - public static IObservable> WhereReasonsAreNot(this IObservable> source, - params ListChangeReason[] reasons) + /// The type of the object. + /// The source. + /// + /// The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received. + /// + /// is null. + public static IObservable> Switch(this IObservable>> sources) { - if (reasons == null) - { - throw new ArgumentNullException(nameof(reasons)); - } - - if (reasons.Length == 0) + if (sources is null) { - throw new ArgumentException("Must enter at least 1 reason", nameof(reasons)); + throw new ArgumentNullException(nameof(sources)); } - var matches = new HashSet(reasons); - return source.Select(updates => - { - var filtered = updates.Where(u => !matches.Contains(u.Reason)).YieldWithoutIndex(); - return new ChangeSet(filtered); - }).NotEmpty(); - } - - #endregion - - #region Buffering - - /// - /// Buffers changes for an intial period only. After the period has elapsed, not further buffering occurs. - /// - /// The source changeset - /// The period to buffer, measure from the time that the first item arrives - /// The scheduler to buffer on - public static IObservable> BufferInitial(this IObservable> source, TimeSpan initalBuffer, IScheduler scheduler = null) - { - return source.DeferUntilLoaded().Publish(shared => - { - var initial = shared.Buffer(initalBuffer, scheduler ?? Scheduler.Default) - .FlattenBufferResult() - .Take(1); - - return initial.Concat(shared); - }); - } - - /// - /// Convert the result of a buffer operation to a change set - /// - /// - /// The source. - /// - public static IObservable> FlattenBufferResult(this IObservable>> source) - { - return source - .Where(x => x.Count != 0) - .Select(updates => new ChangeSet(updates.SelectMany(u => u))); + return new Switch(sources).Run(); } /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. + /// Converts the change set into a fully formed collection. Each change in the source results in a new collection. /// - /// The type of the object. + /// The type of the object. /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// The scheduler. - /// - /// source - public static IObservable> BufferIf([NotNull] this IObservable> source, - [NotNull] IObservable pauseIfTrueSelector, - IScheduler scheduler = null) + /// An observable which emits the read only collection. + public static IObservable> ToCollection(this IObservable> source) { - return BufferIf(source, pauseIfTrueSelector, false, scheduler); + return source.QueryWhenChanged(items => items); } /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. + /// Converts the observable to an observable change set. + /// Change set observes observable change events. /// /// The type of the object. /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// if set to true [intial pause state]. - /// The scheduler. - /// - /// source - public static IObservable> BufferIf([NotNull] this IObservable> source, - [NotNull] IObservable pauseIfTrueSelector, - bool intialPauseState, - IScheduler scheduler = null) + /// The scheduler (only used for time expiry). + /// An observable which emits a change set. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable source, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (pauseIfTrueSelector == null) - { - throw new ArgumentNullException(nameof(pauseIfTrueSelector)); - } - - return BufferIf(source, pauseIfTrueSelector, intialPauseState, null, scheduler); - } - - /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. - /// - /// The type of the object. - /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// Specify a time to ensure the buffer window does not stay open for too long - /// The scheduler. - /// - /// source - public static IObservable> BufferIf(this IObservable> source, - IObservable pauseIfTrueSelector, - TimeSpan? timeOut, - IScheduler scheduler = null) - { - return BufferIf(source, pauseIfTrueSelector, false, timeOut, scheduler); + return ToObservableChangeSet(source, null, -1, scheduler); } /// - /// Batches the underlying updates if a pause signal (i.e when the buffer selector return true) has been received. - /// When a resume signal has been received the batched updates will be fired. + /// Converts the observable to an observable change set, allowing time expiry to be specified. + /// Change set observes observable change events. /// /// The type of the object. /// The source. - /// When true, observable begins to buffer and when false, window closes and buffered result if notified - /// if set to true [intial pause state]. - /// Specify a time to ensure the buffer window does not stay open for too long - /// The scheduler. - /// - /// source - public static IObservable> BufferIf(this IObservable> source, - IObservable pauseIfTrueSelector, - bool intialPauseState, - TimeSpan? timeOut, - IScheduler scheduler = null) + /// Specify on a per object level the maximum time before an object expires from a cache. + /// The scheduler (only used for time expiry). + /// An observable which emits a change set. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable source, Func expireAfter, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (pauseIfTrueSelector == null) + if (expireAfter is null) { - throw new ArgumentNullException(nameof(pauseIfTrueSelector)); + throw new ArgumentNullException(nameof(expireAfter)); } - return new BufferIf(source, pauseIfTrueSelector, intialPauseState, timeOut, scheduler).Run(); + return ToObservableChangeSet(source, expireAfter, -1, scheduler); } /// - /// The latest copy of the cache is exposed for querying after each modification to the underlying data + /// Converts the observable to an observable change set, with a specified limit of how large the list can be. + /// Change set observes observable change events. /// - /// The type of the object. - /// The type of the destination. + /// The type of the object. /// The source. - /// The result selector. - /// - /// - /// source + /// Remove the oldest items when the size has reached this limit. + /// The scheduler (only used for time expiry). + /// An observable which emits a change set. + /// source /// or - /// resultSelector - /// - public static IObservable QueryWhenChanged( - this IObservable> source, - Func, TDestination> resultSelector) + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable source, int limitSizeTo, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (resultSelector == null) - { - throw new ArgumentNullException(nameof(resultSelector)); - } - - return source.QueryWhenChanged().Select(resultSelector); + return ToObservableChangeSet(source, null, limitSizeTo, scheduler); } /// - /// The latest copy of the cache is exposed for querying i) after each modification to the underlying data ii) upon subscription + /// Converts the observable to an observable change set, allowing size and time limit to be specified. + /// Change set observes observable change events. /// /// The type of the object. /// The source. - /// - /// source - public static IObservable> QueryWhenChanged([NotNull] this IObservable> source) + /// Specify on a per object level the maximum time before an object expires from a cache. + /// Remove the oldest items when the size has reached this limit. + /// The scheduler (only used for time expiry). + /// An observable which emits a change set. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable source, Func? expireAfter, int limitSizeTo, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return new QueryWhenChanged(source).Run(); + return new ToObservableChangeSet(source, expireAfter, limitSizeTo, scheduler).Run(); } /// - /// Converts the changeset into a fully formed collection. Each change in the source results in a new collection + /// Converts the observable to an observable change set. + /// Change set observes observable change events. /// - /// The type of the object. + /// The type of the object. /// The source. - /// - public static IObservable> ToCollection(this IObservable> source) + /// The scheduler (only used for time expiry). + /// An observable which emits a change set. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable> source, IScheduler? scheduler = null) { - return source.QueryWhenChanged(items => items); + return ToObservableChangeSet(source, null, -1, scheduler); } /// - /// Converts the changeset into a fully formed sorted collection. Each change in the source results in a new sorted collection + /// Converts the observable to an observable change set, allowing size and time limit to be specified. + /// Change set observes observable change events. /// - /// The type of the object. - /// The sort key + /// The type of the object. /// The source. - /// The sort function - /// The sort order. Defaults to ascending - /// - public static IObservable> ToSortedCollection(this IObservable> source, - Func sort, SortDirection sortOrder = SortDirection.Ascending) + /// Remove the oldest items when the size has reached this limit. + /// The scheduler (only used for time expiry). + /// An observable which emits a change set. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable> source, int limitSizeTo, IScheduler? scheduler = null) { - return source.QueryWhenChanged(query => sortOrder == SortDirection.Ascending - ? new ReadOnlyCollectionLight(query.OrderBy(sort)) - : new ReadOnlyCollectionLight(query.OrderByDescending(sort))); + return ToObservableChangeSet(source, null, limitSizeTo, scheduler); } /// - /// Converts the changeset into a fully formed sorted collection. Each change in the source results in a new sorted collection + /// Converts the observable to an observable change set, allowing size to be specified. + /// Change set observes observable change events. /// - /// The type of the object. + /// The type of the object. /// The source. - /// The sort comparer - /// - public static IObservable> ToSortedCollection(this IObservable> source, - IComparer comparer) + /// Specify on a per object level the maximum time before an object expires from a cache. + /// The scheduler (only used for time expiry). + /// An observable which emits a change set. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable> source, Func expireAfter, IScheduler? scheduler = null) { - return source.QueryWhenChanged(query => - { - var items = query.AsList(); - items.Sort(comparer); - return new ReadOnlyCollectionLight(items); - }); + return ToObservableChangeSet(source, expireAfter, 0, scheduler); } /// - /// Defer the subscribtion until loaded and skip initial changeset + /// Converts the observable to an observable change set, allowing size and time limit to be specified. + /// Change set observes observable change events. /// /// The type of the object. /// The source. - /// - /// source - public static IObservable> SkipInitial(this IObservable> source) + /// Specify on a per object level the maximum time before an object expires from a cache. + /// Remove the oldest items when the size has reached this limit. + /// The scheduler (only used for time expiry). + /// An observable which emits a change set. + /// source + /// or + /// keySelector. + public static IObservable> ToObservableChangeSet(this IObservable> source, Func? expireAfter, int limitSizeTo, IScheduler? scheduler = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return source.DeferUntilLoaded().Skip(1); + return new ToObservableChangeSet(source, expireAfter, limitSizeTo, scheduler).Run(); } /// - /// Defer the subscription until the stream has been inflated with data + /// Limits the size of the result set to the specified number of items. /// - /// The type of the object. + /// The type of the item. /// The source. - /// - public static IObservable> DeferUntilLoaded([NotNull] this IObservable> source) + /// The number of items. + /// An observable which emits the change set. + public static IObservable> Top(this IObservable> source, int numberOfItems) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - return new DeferUntilLoaded(source).Run(); + if (numberOfItems <= 0) + { + throw new ArgumentOutOfRangeException(nameof(numberOfItems), "Number of items should be greater than zero"); + } + + return source.Virtualise(Observable.Return(new VirtualRequest(0, numberOfItems))); } /// - /// Defer the subscription until the cache has been inflated with data + /// Converts the change set into a fully formed sorted collection. Each change in the source results in a new sorted collection. /// - /// The type of the object. + /// The type of the object. + /// The sort key. /// The source. - /// - public static IObservable> DeferUntilLoaded(this IObservableList source) + /// The sort function. + /// The sort order. Defaults to ascending. + /// An observable which emits the read only collection. + public static IObservable> ToSortedCollection(this IObservable> source, Func sort, SortDirection sortOrder = SortDirection.Ascending) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source.Connect().DeferUntilLoaded(); + return source.QueryWhenChanged(query => sortOrder == SortDirection.Ascending ? new ReadOnlyCollectionLight(query.OrderBy(sort)) : new ReadOnlyCollectionLight(query.OrderByDescending(sort))); } - #endregion - - #region Virtualisation / Paging + /// + /// Converts the change set into a fully formed sorted collection. Each change in the source results in a new sorted collection. + /// + /// The type of the object. + /// The source. + /// The sort comparer. + /// An observable which emits the read only collection. + public static IObservable> ToSortedCollection(this IObservable> source, IComparer comparer) + { + return source.QueryWhenChanged( + query => + { + var items = query.AsList(); + items.Sort(comparer); + return new ReadOnlyCollectionLight(items); + }); + } /// - /// Virtualises the source using parameters provided via the requests observable + /// Projects each update item to a new form using the specified transform function. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// The requests. - /// - public static IObservable> Virtualise([NotNull] this IObservable> source, - [NotNull] IObservable requests) + /// The transform factory. + /// Should a new transform be applied when a refresh event is received. + /// An observable which emits the change set. + /// + /// source + /// or + /// valueSelector. + /// + public static IObservable> Transform(this IObservable> source, Func transformFactory, bool transformOnRefresh = false) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (requests == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(requests)); + throw new ArgumentNullException(nameof(transformFactory)); } - return new Virtualiser(source, requests).Run(); + return source.Transform((t, _, _) => transformFactory(t), transformOnRefresh); } /// - /// Limits the size of the result set to the specified number of items + /// Projects each update item to a new form using the specified transform function. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// The number of items. - /// - public static IObservable> Top([NotNull] this IObservable> source, - int numberOfItems) + /// The transform function. + /// Should a new transform be applied when a refresh event is received. + /// A an observable change set of the transformed object. + /// + /// source + /// or + /// valueSelector. + /// + public static IObservable> Transform(this IObservable> source, Func transformFactory, bool transformOnRefresh = false) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (numberOfItems <= 0) + if (transformFactory is null) { - throw new ArgumentOutOfRangeException(nameof(numberOfItems), - "Number of items should be greater than zero"); + throw new ArgumentNullException(nameof(transformFactory)); } - return source.Virtualise(Observable.Return(new VirtualRequest(0, numberOfItems))); + return source.Transform((t, _, idx) => transformFactory(t, idx), transformOnRefresh); } /// - /// Applies paging to the the data source + /// Projects each update item to a new form using the specified transform function. + /// + /// *** Annoyingly when using this overload you will have to explicitly specify the generic type arguments as type inference fails. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// Observable to control page requests - /// - public static IObservable> Page([NotNull] this IObservable> source, - [NotNull] IObservable requests) + /// The transform function. + /// Should a new transform be applied when a refresh event is received. + /// A an observable change set of the transformed object. + /// + /// source + /// or + /// valueSelector. + /// + public static IObservable> Transform(this IObservable> source, Func, TDestination> transformFactory, bool transformOnRefresh = false) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (requests == null) + if (transformFactory is null) { - throw new ArgumentNullException(nameof(requests)); + throw new ArgumentNullException(nameof(transformFactory)); } - return new Pager(source, requests).Run(); + return source.Transform((t, previous, _) => transformFactory(t, previous), transformOnRefresh); } - #endregion - - #region Expiry / size limiter - /// - /// Limits the size of the source cache to the specified limit. - /// Notifies which items have been removed from the source list. + /// Projects each update item to a new form using the specified transform function + /// + /// *** Annoyingly when using this overload you will have to explicitly specify the generic type arguments as type inference fails. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// The size limit. - /// The scheduler. - /// - /// sizeLimit cannot be zero - /// source - /// sizeLimit cannot be zero - public static IObservable> LimitSizeTo([NotNull] this ISourceList source, int sizeLimit, - IScheduler scheduler = null) + /// The transform factory. + /// Should a new transform be applied when a refresh event is received. + /// A an observable change set of the transformed object. + /// + /// source + /// or + /// valueSelector. + /// + public static IObservable> Transform(this IObservable> source, Func, int, TDestination> transformFactory, bool transformOnRefresh = false) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (sizeLimit <= 0) + if (transformFactory is null) { - throw new ArgumentException("sizeLimit cannot be zero", nameof(sizeLimit)); + throw new ArgumentNullException(nameof(transformFactory)); } - var locker = new object(); - var limiter = new LimitSizeTo(source, sizeLimit, scheduler ?? Scheduler.Default, locker); - - return limiter.Run().Synchronize(locker).Do(source.RemoveMany); + return new Transformer(source, transformFactory, transformOnRefresh).Run(); } /// - /// Removes items from the cache according to the value specified by the time selector function + /// Projects each update item to a new form using the specified transform function. /// - /// + /// The type of the source. + /// The type of the destination. /// The source. - /// Selector returning when to expire the item. Return null for non-expiring item - /// The scheduler - /// + /// The transform factory. + /// A an observable change set of the transformed object. /// + /// source + /// or + /// valueSelector. /// - public static IObservable> ExpireAfter([NotNull] this ISourceList source, - [NotNull] Func timeSelector, IScheduler scheduler = null) + public static IObservable> TransformAsync(this IObservable> source, Func> transformFactory) { - return source.ExpireAfter(timeSelector, null, scheduler); + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (transformFactory is null) + { + throw new ArgumentNullException(nameof(transformFactory)); + } + + return new TransformAsync(source, transformFactory).Run(); } /// - /// Removes items from the cache according to the value specified by the time selector function + /// Equivalent to a select many transform. To work, the key must individually identify each child. /// - /// + /// The type of the destination. + /// The type of the source. /// The source. - /// Selector returning when to expire the item. Return null for non-expiring item - /// Enter the polling interval to optimise expiry timers, if ommited 1 timer is created for each unique expiry time - /// The scheduler - /// + /// The selector function which selects the enumerable. + /// Used when an item has been replaced to determine whether child items are the same as previous children. + /// An observable which emits the change set. /// + /// source + /// or + /// manySelector. /// - public static IObservable> ExpireAfter([NotNull] this ISourceList source, - [NotNull] Func timeSelector, TimeSpan? pollingInterval = null, IScheduler scheduler = null) + public static IObservable> TransformMany(this IObservable> source, Func> manySelector, IEqualityComparer? equalityComparer = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (timeSelector == null) + if (manySelector is null) { - throw new ArgumentNullException(nameof(timeSelector)); + throw new ArgumentNullException(nameof(manySelector)); } - var locker = new object(); - var limiter = new ExpireAfter(source, timeSelector, pollingInterval, scheduler ?? Scheduler.Default, - locker); - - return limiter.Run().Synchronize(locker).Do(source.RemoveMany); + return new TransformMany(source, manySelector, equalityComparer).Run(); } - #endregion - - #region Logical collection operators - /// - /// Apply a logical Or operator between the collections. - /// Items which are in any of the sources are included in the result + /// Flatten the nested observable collection, and observe subsequently observable collection changes. /// - /// - /// The source. - /// - public static IObservable> Or([NotNull] this ICollection>> sources) + /// The type of the destination. + /// The type of the source. + /// The source. + /// The selector function which selects the enumerable. + /// Used when an item has been replaced to determine whether child items are the same as previous children. + /// An observable which emits the change set. + public static IObservable> TransformMany(this IObservable> source, Func> manySelector, IEqualityComparer? equalityComparer = null) { - return sources.Combine(CombineOperator.Or); + return new TransformMany(source, manySelector, equalityComparer).Run(); } /// - /// Apply a logical Or operator between the collections. - /// Items which are in any of the sources are included in the result + /// Flatten the nested observable collection, and observe subsequently observable collection changes. /// - /// + /// The type of the destination. + /// The type of the source. /// The source. - /// The others. - /// - public static IObservable> Or([NotNull] this IObservable> source, - params IObservable>[] others) + /// The selector function which selects the enumerable. + /// Used when an item has been replaced to determine whether child items are the same as previous children. + /// An observable which emits the change set. + public static IObservable> TransformMany(this IObservable> source, Func> manySelector, IEqualityComparer? equalityComparer = null) { - return source.Combine(CombineOperator.Or, others); + return new TransformMany(source, manySelector, equalityComparer).Run(); } /// - /// Dynamically apply a logical Or operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result + /// Flatten the nested observable list, and observe subsequent observable collection changes. /// - /// - /// The source. - /// - public static IObservable> Or( - [NotNull] this IObservableList>> sources) + /// The type of the destination. + /// The type of the source. + /// The source. + /// The selector function which selects the enumerable. + /// Used when an item has been replaced to determine whether child items are the same as previous children. + /// An observable which emits the change set. + public static IObservable> TransformMany(this IObservable> source, Func> manySelector, IEqualityComparer? equalityComparer = null) { - return sources.Combine(CombineOperator.Or); + return new TransformMany(source, manySelector, equalityComparer).Run(); } /// - /// Dynamically apply a logical Or operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result + /// Virtualises the source using parameters provided via the requests observable. /// - /// - /// The source. - /// - public static IObservable> Or([NotNull] this IObservableList> sources) + /// The type of the item. + /// The source. + /// The requests. + /// An observable which emits the change set. + public static IObservable> Virtualise(this IObservable> source, IObservable requests) { - return sources.Combine(CombineOperator.Or); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - /// - /// Dynamically apply a logical Or operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result - /// - /// - /// The source. - /// - public static IObservable> Or([NotNull] this IObservableList> sources) - { - return sources.Combine(CombineOperator.Or); + if (requests is null) + { + throw new ArgumentNullException(nameof(requests)); + } + + return new Virtualiser(source, requests).Run(); } /// - /// Apply a logical Xor operator between the collections. - /// Items which are only in one of the sources are included in the result + /// Watches each item in the collection and notifies when any of them has changed. /// - /// + /// The type of the object. /// The source. - /// The others. - /// - public static IObservable> Xor([NotNull] this IObservable> source, - params IObservable>[] others) + /// specify properties to Monitor, or omit to monitor all property changes. + /// An observable which emits the object. + public static IObservable WhenAnyPropertyChanged(this IObservable> source, params string[] propertiesToMonitor) + where TObject : INotifyPropertyChanged { - return source.Combine(CombineOperator.Xor, others); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - /// - /// Apply a logical Xor operator between the collections. - /// Items which are only in one of the sources are included in the result - /// - /// - /// The sources. - /// > - public static IObservable> Xor([NotNull] this ICollection>> sources) - { - return sources.Combine(CombineOperator.Xor); + return source.MergeMany(t => t.WhenAnyPropertyChanged(propertiesToMonitor)); } /// - /// Dynamically apply a logical Xor operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result + /// Watches each item in the collection and notifies when any of them has changed. /// - /// - /// The source. - /// - public static IObservable> Xor( - [NotNull] this IObservableList>> sources) + /// The type of object. + /// The type of the value. + /// The source. + /// The property accessor. + /// if set to true [notify on initial value]. + /// An observable which emits the property value. + public static IObservable> WhenPropertyChanged(this IObservable> source, Expression> propertyAccessor, bool notifyOnInitialValue = true) + where TObject : INotifyPropertyChanged { - return sources.Combine(CombineOperator.Xor); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - /// - /// Dynamically apply a logical Xor operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result - /// - /// - /// The source. - /// - public static IObservable> Xor([NotNull] this IObservableList> sources) - { - return sources.Combine(CombineOperator.Xor); - } + if (propertyAccessor is null) + { + throw new ArgumentNullException(nameof(propertyAccessor)); + } - /// - /// Dynamically apply a logical Xor operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result - /// - /// - /// The source. - /// - public static IObservable> Xor([NotNull] this IObservableList> sources) - { - return sources.Combine(CombineOperator.Xor); + var factory = propertyAccessor.GetFactory(); + return source.MergeMany(t => factory(t, notifyOnInitialValue)); } /// - /// Apply a logical And operator between the collections. - /// Items which are in all of the sources are included in the result + /// Watches each item in the collection and notifies when any of them has changed. /// - /// + /// The type of object. + /// The type of the value. /// The source. - /// The others. - /// - public static IObservable> And([NotNull] this IObservable> source, - params IObservable>[] others) + /// The property accessor. + /// if set to true [notify on initial value]. + /// An observable which emits the value. + public static IObservable WhenValueChanged(this IObservable> source, Expression> propertyAccessor, bool notifyOnInitialValue = true) + where TObject : INotifyPropertyChanged { - return source.Combine(CombineOperator.And, others); - } + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } - /// - /// Apply a logical And operator between the collections. - /// Items which are in all of the sources are included in the result - /// - /// - /// The sources. - /// > - public static IObservable> And([NotNull] this ICollection>> sources) - { - return sources.Combine(CombineOperator.And); - } + if (propertyAccessor is null) + { + throw new ArgumentNullException(nameof(propertyAccessor)); + } - /// - /// Dynamically apply a logical And operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result - /// - /// - /// The source. - /// - public static IObservable> And( - [NotNull] this IObservableList>> sources) - { - return sources.Combine(CombineOperator.And); + var factory = propertyAccessor.GetFactory(); + return source.MergeMany(t => factory(t, notifyOnInitialValue).Select(pv => pv.Value)); } /// - /// Dynamically apply a logical And operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result + /// Includes changes for the specified reasons only. /// - /// - /// The source. - /// - public static IObservable> And([NotNull] this IObservableList> sources) + /// The type of the item. + /// The source. + /// The reasons. + /// An observable which emits the change set. + /// Must enter at least 1 reason. + public static IObservable> WhereReasonsAre(this IObservable> source, params ListChangeReason[] reasons) { - return sources.Combine(CombineOperator.And); + if (reasons is null) + { + throw new ArgumentNullException(nameof(reasons)); + } + + if (reasons.Length == 0) + { + throw new ArgumentException("Must enter at least 1 reason", nameof(reasons)); + } + + var matches = new HashSet(reasons); + return source.Select( + changes => + { + var filtered = changes.Where(change => matches.Contains(change.Reason)).YieldWithoutIndex(); + return new ChangeSet(filtered); + }).NotEmpty(); } /// - /// Dynamically apply a logical And operator between the items in the outer observable list. - /// Items which are in any of the sources are included in the result + /// Excludes updates for the specified reasons. /// - /// - /// The source. - /// - public static IObservable> And([NotNull] this IObservableList> sources) + /// The type of the item. + /// The source. + /// The reasons. + /// An observable which emits the change set. + /// Must enter at least 1 reason. + public static IObservable> WhereReasonsAreNot(this IObservable> source, params ListChangeReason[] reasons) { - return sources.Combine(CombineOperator.And); + if (reasons is null) + { + throw new ArgumentNullException(nameof(reasons)); + } + + if (reasons.Length == 0) + { + throw new ArgumentException("Must enter at least 1 reason", nameof(reasons)); + } + + var matches = new HashSet(reasons); + return source.Select( + updates => + { + var filtered = updates.Where(u => !matches.Contains(u.Reason)).YieldWithoutIndex(); + return new ChangeSet(filtered); + }).NotEmpty(); } /// - /// Apply a logical Except operator between the collections. - /// Items which are in the source and not in the others are included in the result + /// Apply a logical Xor operator between the collections. + /// Items which are only in one of the sources are included in the result. /// - /// + /// The type of the item. /// The source. /// The others. - /// - public static IObservable> Except([NotNull] this IObservable> source, - params IObservable>[] others) + /// An observable which emits the change set. + public static IObservable> Xor(this IObservable> source, params IObservable>[] others) { - return source.Combine(CombineOperator.Except, others); + return source.Combine(CombineOperator.Xor, others); } /// - /// Apply a logical Except operator between the collections. - /// Items which are in the source and not in the others are included in the result + /// Apply a logical Xor operator between the collections. + /// Items which are only in one of the sources are included in the result. /// - /// + /// The type of the item. /// The sources. - /// > - public static IObservable> Except( - [NotNull] this ICollection>> sources) + /// An observable which emits the change set. + public static IObservable> Xor(this ICollection>> sources) { - return sources.Combine(CombineOperator.Except); + return sources.Combine(CombineOperator.Xor); } /// - /// Dynamically apply a logical Except operator. Items from the first observable list are included when an equivalent item does not exist in the other sources. + /// Dynamically apply a logical Xor operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// - /// + /// The type of the item. /// The source. - /// - public static IObservable> Except( - [NotNull] this IObservableList>> sources) + /// An observable which emits the change set. + public static IObservable> Xor(this IObservableList>> sources) { - return sources.Combine(CombineOperator.Except); + return sources.Combine(CombineOperator.Xor); } /// - /// Dynamically apply a logical Except operator. Items from the first observable list are included when an equivalent item does not exist in the other sources. + /// Dynamically apply a logical Xor operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// - /// + /// The type of the item. /// The source. - /// - public static IObservable> Except([NotNull] this IObservableList> sources) + /// An observable which emits the change set. + public static IObservable> Xor(this IObservableList> sources) { - return sources.Combine(CombineOperator.Except); + return sources.Combine(CombineOperator.Xor); } /// - /// Dynamically apply a logical Except operator. Items from the first observable list are included when an equivalent item does not exist in the other sources. + /// Dynamically apply a logical Xor operator between the items in the outer observable list. + /// Items which are in any of the sources are included in the result. /// - /// + /// The type of the item. /// The source. - /// - public static IObservable> Except([NotNull] this IObservableList> sources) + /// An observable which emits the change set. + public static IObservable> Xor(this IObservableList> sources) { - return sources.Combine(CombineOperator.Except); + return sources.Combine(CombineOperator.Xor); } - private static IObservable> Combine( - [NotNull] this ICollection>> sources, - CombineOperator type) + private static IObservable> Combine(this ICollection>> sources, CombineOperator type) { - if (sources == null) + if (sources is null) { throw new ArgumentNullException(nameof(sources)); } @@ -2350,16 +2195,14 @@ private static IObservable> Combine( return new Combiner(sources, type).Run(); } - private static IObservable> Combine([NotNull] this IObservable> source, - CombineOperator type, - params IObservable>[] others) + private static IObservable> Combine(this IObservable> source, CombineOperator type, params IObservable>[] others) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (others == null) + if (others is null) { throw new ArgumentNullException(nameof(others)); } @@ -2373,111 +2216,46 @@ private static IObservable> Combine([NotNull] this IObservable< return new Combiner(items, type).Run(); } - private static IObservable> Combine([NotNull] this IObservableList> sources, - CombineOperator type) + private static IObservable> Combine(this IObservableList> sources, CombineOperator type) { - if (sources == null) + if (sources is null) { throw new ArgumentNullException(nameof(sources)); } - return Observable.Create>(observer => - { - var changesSetList = sources.Connect().Transform(s => s.Connect()).AsObservableList(); - var subscriber = changesSetList.Combine(type).SubscribeSafe(observer); - return new CompositeDisposable(changesSetList, subscriber); - }); + return Observable.Create>( + observer => + { + var changesSetList = sources.Connect().Transform(s => s.Connect()).AsObservableList(); + var subscriber = changesSetList.Combine(type).SubscribeSafe(observer); + return new CompositeDisposable(changesSetList, subscriber); + }); } - private static IObservable> Combine([NotNull] this IObservableList> sources, - CombineOperator type) + private static IObservable> Combine(this IObservableList> sources, CombineOperator type) { - if (sources == null) + if (sources is null) { throw new ArgumentNullException(nameof(sources)); } - return Observable.Create>(observer => - { - var changesSetList = sources.Connect().Transform(s => s.Connect()).AsObservableList(); - var subscriber = changesSetList.Combine(type).SubscribeSafe(observer); - return new CompositeDisposable(changesSetList, subscriber); - }); + return Observable.Create>( + observer => + { + var changesSetList = sources.Connect().Transform(s => s.Connect()).AsObservableList(); + var subscriber = changesSetList.Combine(type).SubscribeSafe(observer); + return new CompositeDisposable(changesSetList, subscriber); + }); } - private static IObservable> Combine( - [NotNull] this IObservableList>> sources, CombineOperator type) + private static IObservable> Combine(this IObservableList>> sources, CombineOperator type) { - if (sources == null) + if (sources is null) { throw new ArgumentNullException(nameof(sources)); } return new DynamicCombiner(sources, type).Run(); } - - #endregion - - #region Switch - - /// - /// Transforms an observable sequence of observable lists into a single sequence - /// producing values only from the most recent observable sequence. - /// Each time a new inner observable sequence is received, unsubscribe from the - /// previous inner observable sequence and clear the existing result set - /// - /// The type of the object. - /// The source. - /// - /// The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received. - /// - /// - /// is null. - public static IObservable> Switch(this IObservable> sources) - { - if (sources == null) - { - throw new ArgumentNullException(nameof(sources)); - } - - return sources.Select(cache => cache.Connect()).Switch(); - } - - /// - /// Transforms an observable sequence of observable changes sets into an observable sequence - /// producing values only from the most recent observable sequence. - /// Each time a new inner observable sequence is received, unsubscribe from the - /// previous inner observable sequence and clear the existing resukt set - /// - /// The type of the object. - /// The source. - /// - /// The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received. - /// - /// - /// is null. - public static IObservable> Switch(this IObservable>> sources) - { - if (sources == null) - { - throw new ArgumentNullException(nameof(sources)); - } - - return new Switch(sources).Run(); - } - - #endregion - - #region Start with - - /// - /// Prepends an empty changeset to the source - /// - public static IObservable> StartWithEmpty(this IObservable> source) - { - return source.StartWith(ChangeSet.Empty); - } - - #endregion } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/PageChangeSet.cs b/src/DynamicData/List/PageChangeSet.cs index 259a73280..51de69dd9 100644 --- a/src/DynamicData/List/PageChangeSet.cs +++ b/src/DynamicData/List/PageChangeSet.cs @@ -1,10 +1,11 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections; using System.Collections.Generic; + using DynamicData.Operators; // ReSharper disable once CheckNamespace @@ -21,17 +22,13 @@ public PageChangeSet(IChangeSet virtualChangeSet, IPageResponse response) Response = response ?? throw new ArgumentNullException(nameof(response)); } - public IPageResponse Response { get; } - - #region Delegating members - - int IChangeSet.Adds => _virtualChangeSet.Adds; + public int Count => _virtualChangeSet.Count; - int IChangeSet.Removes => _virtualChangeSet.Removes; + public int Refreshes => _virtualChangeSet.Refreshes; - int IChangeSet.Moves => _virtualChangeSet.Moves; + public IPageResponse Response { get; } - public int Count => _virtualChangeSet.Count; + int IChangeSet.Adds => _virtualChangeSet.Adds; int IChangeSet.Capacity { @@ -39,13 +36,13 @@ int IChangeSet.Capacity set => _virtualChangeSet.Capacity = value; } - int IChangeSet.Replaced => _virtualChangeSet.Replaced; + int IChangeSet.Moves => _virtualChangeSet.Moves; - int IChangeSet.TotalChanges => _virtualChangeSet.TotalChanges; + int IChangeSet.Removes => _virtualChangeSet.Removes; - public int Refreshes => _virtualChangeSet.Refreshes; + int IChangeSet.Replaced => _virtualChangeSet.Replaced; - #endregion + int IChangeSet.TotalChanges => _virtualChangeSet.TotalChanges; public IEnumerator> GetEnumerator() { diff --git a/src/DynamicData/List/RangeChange.cs b/src/DynamicData/List/RangeChange.cs index f2c8066c4..2cb056346 100644 --- a/src/DynamicData/List/RangeChange.cs +++ b/src/DynamicData/List/RangeChange.cs @@ -1,20 +1,23 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System.Collections; using System.Collections.Generic; + using DynamicData.Kernel; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Multiple change container + /// Multiple change container. /// - /// + /// The type of the item. public sealed class RangeChange : IEnumerable { + private readonly List _items; + /// /// Initializes a new instance of the class. /// @@ -27,43 +30,26 @@ public RangeChange(IEnumerable items, int index = -1) } /// - /// Adds the specified item to the range. + /// Initializes a new instance of the class. /// - /// The item. - public void Add(T item) + private RangeChange() { - _items.Add(item); + _items = new List(); + Index = -1; } /// - /// Inserts the item in the range at the specified index. + /// Gets a Empty version of the RangeChange. /// - /// The index. - /// The item. - public void Insert(int index, T item) - { - _items.Insert(index, item); - } + public static RangeChange Empty { get; } = new(); /// - /// Sets the index of the starting index of the range - /// - /// The index. - /// - public void SetStartingIndex(int index) - { - Index = index; - } - - private readonly List _items; - - /// - /// The total update count + /// Gets the total update count. /// public int Count => _items.Count; /// - /// Gets the index initial index i.e. for the initial starting point of the range insertion + /// Gets the index initial index i.e. for the initial starting point of the range insertion. /// /// /// The index. @@ -71,25 +57,51 @@ public void SetStartingIndex(int index) public int Index { get; private set; } /// - /// Gets the enumerator. + /// Adds the specified item to the range. /// - /// + /// The item. + public void Add(T item) + { + _items.Add(item); + } + + /// public IEnumerator GetEnumerator() { return _items.GetEnumerator(); } - IEnumerator IEnumerable.GetEnumerator() + /// + /// Inserts the item in the range at the specified index. + /// + /// The index. + /// The item. + public void Insert(int index, T item) { - return GetEnumerator(); + _items.Insert(index, item); } /// - /// Returns a that represents this instance. + /// Sets the index of the starting index of the range. + /// + /// The index. + public void SetStartingIndex(int index) + { + Index = index; + } + + /// + /// Returns a that represents this instance. /// /// - /// A that represents this instance. + /// A that represents this instance. /// public override string ToString() => $"Range<{typeof(T).Name}>. Count={Count}"; + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/SortException.cs b/src/DynamicData/List/SortException.cs index 934a6a355..983968c8c 100644 --- a/src/DynamicData/List/SortException.cs +++ b/src/DynamicData/List/SortException.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,7 +8,7 @@ namespace DynamicData { /// - /// Thrown when an exception occurs within the sort operators + /// Thrown when an exception occurs within the sort operators. /// [Serializable] public class SortException : Exception @@ -22,18 +22,31 @@ public SortException(string message) { } + /// + /// Initializes a new instance of the class. + /// public SortException() { } + /// + /// Initializes a new instance of the class. + /// + /// A message about the exception. + /// A inner exception with further information. public SortException(string message, Exception innerException) : base(message, innerException) { } + /// + /// Initializes a new instance of the class. + /// + /// The serialization info. + /// The serialization context. protected SortException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) : base(serializationInfo, streamingContext) { } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/SortOptions.cs b/src/DynamicData/List/SortOptions.cs index d1364f8c0..a6fa414a2 100644 --- a/src/DynamicData/List/SortOptions.cs +++ b/src/DynamicData/List/SortOptions.cs @@ -1,12 +1,13 @@ - -// ReSharper disable once CheckNamespace +// Copyright (c) 2011-2020 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. using System.Diagnostics.CodeAnalysis; namespace DynamicData { /// - /// Options for sorting + /// Options for sorting. /// [SuppressMessage("Design", "CA1717: Only flags should have plural names", Justification = "Backwards compatibility")] public enum SortOptions @@ -21,4 +22,4 @@ public enum SortOptions /// UseBinarySearch } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/SourceList.cs b/src/DynamicData/List/SourceList.cs index 01c9d9f21..94855b2ec 100644 --- a/src/DynamicData/List/SourceList.cs +++ b/src/DynamicData/List/SourceList.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -7,24 +7,29 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; -using DynamicData.Annotations; + using DynamicData.List.Internal; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// An editable observable list + /// An editable observable list. /// /// The type of the object. public sealed class SourceList : ISourceList { private readonly ISubject> _changes = new Subject>(); - private readonly Subject> _changesPreview = new Subject>(); - private readonly Lazy> _countChanged = new Lazy>(() => new Subject()); - private readonly ReaderWriter _readerWriter = new ReaderWriter(); + + private readonly Subject> _changesPreview = new(); + private readonly IDisposable _cleanUp; - private readonly object _locker = new object(); + + private readonly Lazy> _countChanged = new(() => new Subject()); + + private readonly object _locker = new(); + + private readonly ReaderWriter _readerWriter = new(); private int _editLevel; @@ -32,53 +37,95 @@ public sealed class SourceList : ISourceList /// Initializes a new instance of the class. /// /// The source. - public SourceList(IObservable> source = null) + public SourceList(IObservable>? source = null) { - var loader = source == null ? Disposable.Empty : LoadFromSource(source); + var loader = source is null ? Disposable.Empty : LoadFromSource(source); + + _cleanUp = Disposable.Create( + () => + { + loader.Dispose(); + OnCompleted(); + if (_countChanged.IsValueCreated) + { + _countChanged.Value.OnCompleted(); + } + }); + } - _cleanUp = Disposable.Create(() => + /// + public int Count => _readerWriter.Count; + + /// + public IObservable CountChanged => + Observable.Create( + observer => + { + lock (_locker) + { + var source = _countChanged.Value.StartWith(_readerWriter.Count).DistinctUntilChanged(); + return source.SubscribeSafe(observer); + } + }); + + /// + public IEnumerable Items => _readerWriter.Items; + + /// + public IObservable> Connect(Func? predicate = null) + { + var observable = Observable.Create>( + observer => + { + lock (_locker) + { + if (_readerWriter.Items.Length > 0) + { + observer.OnNext( + new ChangeSet + { + new(ListChangeReason.AddRange, _readerWriter.Items) + }); + } + + var source = _changes.Finally(observer.OnCompleted); + + return source.SubscribeSafe(observer); + } + }); + + if (predicate is not null) { - loader.Dispose(); - OnCompleted(); - if (_countChanged.IsValueCreated) - { - _countChanged.Value.OnCompleted(); - } - }); + observable = new FilterStatic(observable, predicate).Run(); + } + + return observable; } - private IDisposable LoadFromSource(IObservable> source) + /// + public void Dispose() { - return source.Synchronize(_locker) - .Finally(OnCompleted) - .Select(_readerWriter.Write) - .Subscribe(InvokeNext, OnError, OnCompleted); + _cleanUp.Dispose(); + _changesPreview.Dispose(); } /// - public void Edit([NotNull] Action> updateAction) + public void Edit(Action> updateAction) { - if (updateAction == null) + if (updateAction is null) { throw new ArgumentNullException(nameof(updateAction)); } lock (_locker) { - IChangeSet changes = null; + IChangeSet? changes = null; _editLevel++; if (_editLevel == 1) { - if (_changesPreview.HasObservers) - { - changes = _readerWriter.WriteWithPreview(updateAction, InvokeNextPreview); - } - else - { - changes = _readerWriter.Write(updateAction); - } + changes = _changesPreview.HasObservers ? _readerWriter.WriteWithPreview(updateAction, InvokeNextPreview) : _readerWriter.Write(updateAction); } else { @@ -87,24 +134,24 @@ public void Edit([NotNull] Action> updateAction) _editLevel--; - if (_editLevel == 0) + if (changes is not null && _editLevel == 0) { InvokeNext(changes); } } } - private void InvokeNextPreview(IChangeSet changes) + /// + public IObservable> Preview(Func? predicate = null) { - if (changes.Count == 0) - { - return; - } + IObservable> observable = _changesPreview; - lock (_locker) + if (predicate is not null) { - _changesPreview.OnNext(changes); + observable = new FilterStatic(observable, predicate).Run(); } + + return observable; } private void InvokeNext(IChangeSet changes) @@ -125,88 +172,40 @@ private void InvokeNext(IChangeSet changes) } } - private void OnCompleted() + private void InvokeNextPreview(IChangeSet changes) { - lock (_locker) + if (changes.Count == 0) { - _changesPreview.OnCompleted(); - _changes.OnCompleted(); + return; } - } - private void OnError(Exception exception) - { lock (_locker) { - _changesPreview.OnError(exception); - _changes.OnError(exception); + _changesPreview.OnNext(changes); } } - /// - public IEnumerable Items => _readerWriter.Items; - - /// - public int Count => _readerWriter.Count; - - /// - public IObservable CountChanged => Observable.Create(observer => + private IDisposable LoadFromSource(IObservable> source) { - lock (_locker) - { - var source = _countChanged.Value.StartWith(_readerWriter.Count).DistinctUntilChanged(); - return source.SubscribeSafe(observer); - } - }); + return source.Synchronize(_locker).Finally(OnCompleted).Select(_readerWriter.Write).Subscribe(InvokeNext, OnError, OnCompleted); + } - /// - public IObservable> Connect(Func predicate = null) + private void OnCompleted() { - var observable = Observable.Create>(observer => - { - lock (_locker) - { - if (_readerWriter.Items.Length > 0) - { - observer.OnNext( - new ChangeSet() - { - new Change(ListChangeReason.AddRange, _readerWriter.Items) - }); - } - - var source = _changes.Finally(observer.OnCompleted); - - return source.SubscribeSafe(observer); - } - }); - - if (predicate != null) + lock (_locker) { - observable = new FilterStatic(observable, predicate).Run(); + _changesPreview.OnCompleted(); + _changes.OnCompleted(); } - - return observable; } - /// - public IObservable> Preview(Func predicate = null) + private void OnError(Exception exception) { - IObservable> observable = _changesPreview; - - if (predicate != null) + lock (_locker) { - observable = new FilterStatic(observable, predicate).Run(); + _changesPreview.OnError(exception); + _changes.OnError(exception); } - - return observable; - } - - /// - public void Dispose() - { - _cleanUp.Dispose(); - _changesPreview?.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/SourceListEditConvenienceEx.cs b/src/DynamicData/List/SourceListEditConvenienceEx.cs index 7e276e214..820d161df 100644 --- a/src/DynamicData/List/SourceListEditConvenienceEx.cs +++ b/src/DynamicData/List/SourceListEditConvenienceEx.cs @@ -1,55 +1,60 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; using System.Collections.Generic; -using DynamicData.Annotations; + using DynamicData.List.Internal; // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Convenience methods for a source list + /// Convenience methods for a source list. /// public static class SourceListEditConvenienceEx { /// - /// Loads the list with the specified items in an optimised manner i.e. calculates the differences between the old and new items - /// in the list and amends only the differences + /// Adds the specified item to the source list. /// - /// The type of the object. - /// The source. - /// - /// The equality comparer used to determine whether an item has changed - /// source - public static void EditDiff([NotNull] this ISourceList source, - [NotNull] IEnumerable allItems, - IEqualityComparer equalityComparer = null) + /// The item type. + /// The source list. + /// The item to add. + public static void Add(this ISourceList source, T item) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (allItems == null) + source.Edit(list => list.Add(item)); + } + + /// + /// Adds the specified items to the source list. + /// + /// The item type. + /// The source. + /// The items. + public static void AddRange(this ISourceList source, IEnumerable items) + { + if (source is null) { - throw new ArgumentNullException(nameof(allItems)); + throw new ArgumentNullException(nameof(source)); } - var editDiff = new EditDiff(source, equalityComparer); - editDiff.Edit(allItems); + source.Edit(list => list.AddRange(items)); } /// - /// Clears all items from the specified source list + /// Clears all items from the specified source list. /// - /// - /// The source. - public static void Clear([NotNull] this ISourceList source) + /// The item type. + /// The source to clear. + public static void Clear(this ISourceList source) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -58,31 +63,39 @@ public static void Clear([NotNull] this ISourceList source) } /// - /// Adds the specified item to the source list + /// Loads the list with the specified items in an optimised manner i.e. calculates the differences between the old and new items + /// in the list and amends only the differences. /// - /// + /// The type of the object. /// The source. - /// The item. - public static void Add([NotNull] this ISourceList source, T item) + /// The items to compare against and performing a delta. + /// The equality comparer used to determine whether an item has changed. + public static void EditDiff(this ISourceList source, IEnumerable allItems, IEqualityComparer? equalityComparer = null) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(list => list.Add(item)); + if (allItems is null) + { + throw new ArgumentNullException(nameof(allItems)); + } + + var editDiff = new EditDiff(source, equalityComparer); + editDiff.Edit(allItems); } /// - /// Adds the specified item to the source list + /// Adds the specified item to the source list. /// - /// + /// The item type. /// The source. + /// The index of the item. /// The item. - /// The index. - public static void Insert([NotNull] this ISourceList source, int index, T item) + public static void Insert(this ISourceList source, int index, T item) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -91,48 +104,50 @@ public static void Insert([NotNull] this ISourceList source, int index, T } /// - /// Adds the specified items to the source list + /// Inserts the elements of a collection into the at the specified index. /// - /// + /// The item type. /// The source. /// The items. - public static void AddRange([NotNull] this ISourceList source, IEnumerable items) + /// The zero-based index at which the new elements should be inserted. + public static void InsertRange(this ISourceList source, IEnumerable items, int index) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(list => list.AddRange(items)); + source.Edit(list => list.AddRange(items, index)); } /// - /// Inserts the elements of a collection into the at the specified index. + /// Moves an item from the original to the destination index. /// - /// + /// The item type. /// The source. - /// The items. - /// The zero-based index at which the new elements should be inserted. - public static void InsertRange([NotNull] this ISourceList source, IEnumerable items, int index) + /// The original. + /// The destination. + public static void Move(this ISourceList source, int original, int destination) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(list => list.AddRange(items, index)); + source.Edit(list => list.Move(original, destination)); } /// - /// Removes the specified item from the source list + /// Removes the specified item from the source list. /// - /// + /// The item type. /// The source. /// The item. - public static bool Remove([NotNull] this ISourceList source, T item) + /// If the item was removed. + public static bool Remove(this ISourceList source, T item) { bool removed = false; - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -142,50 +157,49 @@ public static bool Remove([NotNull] this ISourceList source, T item) } /// - /// Removes the items from source in an optimised manner + /// Removes the element at the specified index. /// - /// + /// The item type. /// The source. - /// The items to remove. - /// - public static void RemoveMany([NotNull] this ISourceList source, IEnumerable itemsToRemove) + /// The index. + public static void RemoveAt(this ISourceList source, int index) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(list => list.RemoveMany(itemsToRemove)); + source.Edit(list => list.RemoveAt(index)); } /// - /// Moves an item from the original to the destination index + /// Removes the items from source in an optimised manner. /// - /// The source. - /// The original. - /// The destination. - public static void Move([NotNull] this ISourceList source, int original, int destination) + /// The item type. + /// The source. + /// The items to remove. + public static void RemoveMany(this ISourceList source, IEnumerable itemsToRemove) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - source.Edit(list => list.Move(original, destination)); + source.Edit(list => list.RemoveMany(itemsToRemove)); } /// - /// Removes a range of elements from the . + /// Removes a range of elements from the . /// - /// + /// The item type. /// The source. /// The zero-based starting index of the range of elements to remove. /// The number of elements to remove. - /// is less than 0.-or- is less than 0. - /// and do not denote a valid range of elements in the . - public static void RemoveRange([NotNull] this ISourceList source, int index, int count) + /// is less than 0.-or- is less than 0. + /// and do not denote a valid range of elements in the . + public static void RemoveRange(this ISourceList source, int index, int count) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -194,31 +208,15 @@ public static void RemoveRange([NotNull] this ISourceList source, int inde } /// - /// Removes the element at the specified index - /// - /// - /// The source. - /// The index. - public static void RemoveAt([NotNull] this ISourceList source, int index) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - source.Edit(list => list.RemoveAt(index)); - } - - /// - /// Replaces the specified original with the destination object + /// Replaces the specified original with the destination object. /// - /// + /// The item type. /// The source. /// The original. /// The destination. - public static void Replace([NotNull] this ISourceList source, T original, T destination) + public static void Replace(this ISourceList source, T original, T destination) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -227,15 +225,15 @@ public static void Replace([NotNull] this ISourceList source, T original, } /// - /// Replaces the item at the specified index with the new item + /// Replaces the item at the specified index with the new item. /// - /// + /// The item type. /// The source. /// The index. /// The item. - public static void ReplaceAt([NotNull] this ISourceList source, int index, T item) + public static void ReplaceAt(this ISourceList source, int index, T item) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } @@ -243,4 +241,4 @@ public static void ReplaceAt([NotNull] this ISourceList source, int index, source.Edit(list => list[index] = item); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/SourceListEx.cs b/src/DynamicData/List/SourceListEx.cs index 38184aefe..deae9ac5e 100644 --- a/src/DynamicData/List/SourceListEx.cs +++ b/src/DynamicData/List/SourceListEx.cs @@ -1,16 +1,14 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. using System; -using DynamicData.Annotations; - // ReSharper disable once CheckNamespace namespace DynamicData { /// - /// Source list extensions + /// Source list extensions. /// public static class SourceListEx { @@ -23,18 +21,15 @@ public static class SourceListEx /// The type of the destination. /// The source. /// The conversion factory. - /// - /// - /// - public static IObservable> Cast([NotNull] this ISourceList source, - [NotNull] Func conversionFactory) + /// An observable which emits that change set. + public static IObservable> Cast(this ISourceList source, Func conversionFactory) { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (conversionFactory == null) + if (conversionFactory is null) { throw new ArgumentNullException(nameof(conversionFactory)); } @@ -42,4 +37,4 @@ public static IObservable> Cast( return source.Connect().Cast(conversionFactory); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Tests/ChangeSetAggregator.cs b/src/DynamicData/List/Tests/ChangeSetAggregator.cs index 4fea26a4d..54366783e 100644 --- a/src/DynamicData/List/Tests/ChangeSetAggregator.cs +++ b/src/DynamicData/List/Tests/ChangeSetAggregator.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -6,24 +6,22 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; -using DynamicData.Diagnostics; // ReSharper disable once CheckNamespace namespace DynamicData.Tests { /// - /// Aggregates all events and statistics for a changeset to help assertions when testing + /// Aggregates all events and statistics for a change set to help assertions when testing. /// /// The type of the object. public class ChangeSetAggregator : IDisposable { private readonly IDisposable _disposer; - private readonly IList> _messages = new List>(); - private Exception _error; + private bool _isDisposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The source. public ChangeSetAggregator(IObservable> source) @@ -32,26 +30,32 @@ public ChangeSetAggregator(IObservable> source) Data = published.AsObservableList(); - var results = published.Subscribe(updates => _messages.Add(updates), ex => _error = ex); + var results = published.Subscribe(updates => Messages.Add(updates), ex => Exception = ex); var connected = published.Connect(); - _disposer = Disposable.Create(() => - { - Data.Dispose(); - connected.Dispose(); - results.Dispose(); - }); + _disposer = Disposable.Create( + () => + { + Data.Dispose(); + connected.Dispose(); + results.Dispose(); + }); } /// - /// A clone of the data + /// Gets or sets the exception. + /// + public Exception? Exception { get; set; } + + /// + /// Gets a clone of the data. /// public IObservableList Data { get; } /// - /// All message received + /// Gets all message received. /// - public IList> Messages => _messages; + public IList> Messages { get; } = new List>(); /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -62,6 +66,10 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Disposes of managed and unmanaged resources. + /// + /// If the method is being called by the Dispose method. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -73,8 +81,8 @@ protected virtual void Dispose(bool isDisposing) if (isDisposing) { - _disposer?.Dispose(); + _disposer.Dispose(); } } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/Tests/TestEx.cs b/src/DynamicData/List/Tests/ListTextEx.cs similarity index 61% rename from src/DynamicData/List/Tests/TestEx.cs rename to src/DynamicData/List/Tests/ListTextEx.cs index 42435d7dc..2d0b161c3 100644 --- a/src/DynamicData/List/Tests/TestEx.cs +++ b/src/DynamicData/List/Tests/ListTextEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,18 +8,19 @@ namespace DynamicData.Tests { /// - /// Test extensions + /// Test extensions. /// public static class ListTextEx { /// - /// Aggregates all events and statistics for a changeset to help assertions when testing + /// Aggregates all events and statistics for a change set to help assertions when testing. /// + /// The source observable. /// The type of the object. - /// + /// The change set aggregator. public static ChangeSetAggregator AsAggregator(this IObservable> source) { - return new ChangeSetAggregator(source); + return new(source); } } -} +} \ No newline at end of file diff --git a/src/DynamicData/List/UnspecifiedIndexException.cs b/src/DynamicData/List/UnspecifiedIndexException.cs index c2b1e2253..2be427d64 100644 --- a/src/DynamicData/List/UnspecifiedIndexException.cs +++ b/src/DynamicData/List/UnspecifiedIndexException.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -8,7 +8,7 @@ namespace DynamicData { /// - /// Thrown when an index is expected but not specified + /// Thrown when an index is expected but not specified. /// [Serializable] public class UnspecifiedIndexException : Exception @@ -39,6 +39,11 @@ public UnspecifiedIndexException(string message, Exception innerException) { } + /// + /// Initializes a new instance of the class. + /// + /// The serialization info. + /// The serialization context. protected UnspecifiedIndexException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) : base(serializationInfo, streamingContext) { diff --git a/src/DynamicData/List/VirtualChangeSet.cs b/src/DynamicData/List/VirtualChangeSet.cs index 26361755c..a3354a892 100644 --- a/src/DynamicData/List/VirtualChangeSet.cs +++ b/src/DynamicData/List/VirtualChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -20,31 +20,27 @@ public VirtualChangeSet(IChangeSet virtualChangeSet, IVirtualResponse respons Response = response ?? throw new ArgumentNullException(nameof(response)); } - public IVirtualResponse Response { get; } + public int Refreshes => _virtualChangeSet.Refreshes; - #region Delegating members + public IVirtualResponse Response { get; } int IChangeSet.Adds => _virtualChangeSet.Adds; - int IChangeSet.Removes => _virtualChangeSet.Removes; - - int IChangeSet.Moves => _virtualChangeSet.Moves; - - int IChangeSet.Count => _virtualChangeSet.Count; - int IChangeSet.Capacity { get => _virtualChangeSet.Capacity; set => _virtualChangeSet.Capacity = value; } - int IChangeSet.Replaced => _virtualChangeSet.Replaced; + int IChangeSet.Count => _virtualChangeSet.Count; - int IChangeSet.TotalChanges => _virtualChangeSet.TotalChanges; + int IChangeSet.Moves => _virtualChangeSet.Moves; - public int Refreshes => _virtualChangeSet.Refreshes; + int IChangeSet.Removes => _virtualChangeSet.Removes; + + int IChangeSet.Replaced => _virtualChangeSet.Replaced; - #endregion + int IChangeSet.TotalChanges => _virtualChangeSet.TotalChanges; public IEnumerator> GetEnumerator() { diff --git a/src/DynamicData/ObservableChangeSet.cs b/src/DynamicData/ObservableChangeSet.cs index 6555fb72f..a0b007b84 100644 --- a/src/DynamicData/ObservableChangeSet.cs +++ b/src/DynamicData/ObservableChangeSet.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. @@ -11,97 +11,97 @@ namespace DynamicData { /// - /// Creation methods for observable change sets + /// Creation methods for observable change sets. /// public static class ObservableChangeSet { - - #region Cache Create Methods - /// /// Creates an observable cache from a specified Subscribe method implementation. /// - /// The type of the elements contained in the observable cache - /// The type of the specified key + /// The type of the elements contained in the observable cache. + /// The type of the specified key. /// Implementation of the resulting observable cache's Subscribe method. /// The key selector. /// The observable cache with the specified implementation for the Subscribe method. public static IObservable> Create(Func, Action> subscribe, Func keySelector) + where TKey : notnull { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return Create(cache => - { - var action = subscribe(cache); - return Disposable.Create(() => { action?.Invoke(); }); - },keySelector); + return Create( + cache => + { + var action = subscribe(cache); + return Disposable.Create(() => action?.Invoke()); + }, + keySelector); } /// /// Creates an observable cache from a specified Subscribe method implementation. /// - /// The type of the elements contained in the observable cache - /// The type of the specified key + /// The type of the elements contained in the observable cache. + /// The type of the specified key. /// Implementation of the resulting observable cache's Subscribe method. /// The key selector. /// The observable cache with the specified implementation for the Subscribe method. public static IObservable> Create(Func, IDisposable> subscribe, Func keySelector) + where TKey : notnull { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return Observable.Create>(observer => - { - var cache = new SourceCache(keySelector); - var disposable = new SingleAssignmentDisposable(); - - try - { - disposable.Disposable = subscribe(cache); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(disposable, - Disposable.Create(observer.OnCompleted), - cache.Connect().SubscribeSafe(observer), - cache); - }); + return Observable.Create>( + observer => + { + var cache = new SourceCache(keySelector); + var disposable = new SingleAssignmentDisposable(); + + try + { + disposable.Disposable = subscribe(cache); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable(disposable, Disposable.Create(observer.OnCompleted), cache.Connect().SubscribeSafe(observer), cache); + }); } /// /// Creates an observable cache from a specified Subscribe method implementation. /// - /// The type of the elements contained in the observable cache - /// The type of the specified key + /// The type of the elements contained in the observable cache. + /// The type of the specified key. /// Implementation of the resulting observable cache's Subscribe method. /// The key selector. /// The observable cache with the specified implementation for the Subscribe method. public static IObservable> Create(Func, Task> subscribe, Func keySelector) + where TKey : notnull { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } @@ -112,410 +112,428 @@ public static IObservable> Create(Func< /// /// Creates an observable cache from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation. /// - /// The type of the elements contained in the observable cache - /// The type of the specified key + /// The type of the elements contained in the observable cache. + /// The type of the specified key. /// Implementation of the resulting observable cache's Subscribe method. /// The key selector. /// The observable cache with the specified implementation for the Subscribe method. public static IObservable> Create(Func, CancellationToken, Task> subscribe, Func keySelector) + where TKey : notnull { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return Observable.Create>(async (observer, ct) => - { - var cache = new SourceCache(keySelector); - var disposable = new SingleAssignmentDisposable(); - - try - { - disposable.Disposable = await subscribe(cache, ct).ConfigureAwait(false); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(cache.Connect().SubscribeSafe(observer), - cache, - disposable, - Disposable.Create(observer.OnCompleted)); - }); + return Observable.Create>( + async (observer, ct) => + { + var cache = new SourceCache(keySelector); + var disposable = new SingleAssignmentDisposable(); + + try + { + disposable.Disposable = await subscribe(cache, ct).ConfigureAwait(false); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable(cache.Connect().SubscribeSafe(observer), cache, disposable, Disposable.Create(observer.OnCompleted)); + }); } /// /// Creates an observable cache from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation. /// - /// The type of the elements contained in the observable cache - /// The type of the specified key + /// The type of the elements contained in the observable cache. + /// The type of the specified key. /// Implementation of the resulting observable cache's Subscribe method. /// The key selector. /// The observable cache with the specified implementation for the Subscribe method. public static IObservable> Create(Func, Task> subscribe, Func keySelector) + where TKey : notnull { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return Create((list, ct) => subscribe(list), keySelector); + return Create((list, _) => subscribe(list), keySelector); } /// /// Creates an observable cache from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation. /// - /// The type of the elements contained in the observable cache - /// The type of the specified key + /// The type of the elements contained in the observable cache. + /// The type of the specified key. /// Implementation of the resulting observable cache's Subscribe method. /// The key selector. /// The observable cache with the specified implementation for the Subscribe method. public static IObservable> Create(Func, CancellationToken, Task> subscribe, Func keySelector) + where TKey : notnull { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return Observable.Create>(async (observer, ct) => - { - var cache = new SourceCache(keySelector); - Action disposeAction = null; - - try - { - disposeAction = await subscribe(cache, ct).ConfigureAwait(false); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(cache.Connect().SubscribeSafe(observer), - cache, Disposable.Create(() => - { - observer.OnCompleted(); - disposeAction?.Invoke(); - })); - }); + return Observable.Create>( + async (observer, ct) => + { + var cache = new SourceCache(keySelector); + Action? disposeAction = null; + + try + { + disposeAction = await subscribe(cache, ct).ConfigureAwait(false); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable( + cache.Connect().SubscribeSafe(observer), + cache, + Disposable.Create( + () => + { + observer.OnCompleted(); + disposeAction?.Invoke(); + })); + }); } /// /// Creates an observable cache from a specified asynchronous Subscribe method. /// - /// The type of the elements contained in the observable cache - /// The type of the specified key + /// The type of the elements contained in the observable cache. + /// The type of the specified key. /// Implementation of the resulting observable cache's Subscribe method. /// The key selector. /// The observable cache with the specified implementation for the Subscribe method. public static IObservable> Create(Func, Task> subscribe, Func keySelector) + where TKey : notnull { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return Observable.Create>(async observer => - { - var cache = new SourceCache(keySelector); - - try - { - await subscribe(cache).ConfigureAwait(false); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(cache.Connect().SubscribeSafe(observer), - cache, - Disposable.Create(observer.OnCompleted)); - }); + return Observable.Create>( + async observer => + { + var cache = new SourceCache(keySelector); + + try + { + await subscribe(cache).ConfigureAwait(false); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable(cache.Connect().SubscribeSafe(observer), cache, Disposable.Create(observer.OnCompleted)); + }); } /// /// Creates an observable cache from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation. /// - /// The type of the elements contained in the observable cache - /// The type of the specified key + /// The type of the elements contained in the observable cache. + /// The type of the specified key. /// Implementation of the resulting observable cache's Subscribe method. /// The key selector. /// The observable cache with the specified implementation for the Subscribe method. public static IObservable> Create(Func, CancellationToken, Task> subscribe, Func keySelector) + where TKey : notnull { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - if (keySelector == null) + if (keySelector is null) { throw new ArgumentNullException(nameof(keySelector)); } - return Observable.Create>(async (observer, ct) => - { - var cache = new SourceCache(keySelector); - - try - { - await subscribe(cache, ct).ConfigureAwait(false); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(cache.Connect().SubscribeSafe(observer), - cache, - Disposable.Create(observer.OnCompleted)); - }); + return Observable.Create>( + async (observer, ct) => + { + var cache = new SourceCache(keySelector); + + try + { + await subscribe(cache, ct).ConfigureAwait(false); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable(cache.Connect().SubscribeSafe(observer), cache, Disposable.Create(observer.OnCompleted)); + }); } - #endregion - - #region List Create Methods - /// /// Creates an observable list from a specified Subscribe method implementation. /// - /// The type of the elements contained in the observable list + /// The type of the elements contained in the observable list. /// Implementation of the resulting observable list's Subscribe method. /// The observable list with the specified implementation for the Subscribe method. public static IObservable> Create(Func, Action> subscribe) { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - return Create(list => - { - var action = subscribe(list); - return Disposable.Create(() => { action?.Invoke(); }); - }); + return Create( + list => + { + var action = subscribe(list); + return Disposable.Create(() => action?.Invoke()); + }); } /// /// Creates an observable list from a specified Subscribe method implementation. /// - /// The type of the elements contained in the observable list + /// The type of the elements contained in the observable list. /// Implementation of the resulting observable list's Subscribe method. /// The observable list with the specified implementation for the Subscribe method. public static IObservable> Create(Func, IDisposable> subscribe) { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - return Observable.Create>(observer => - { - var list = new SourceList(); - IDisposable disposeAction = null; - - try - { - disposeAction = subscribe(list); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(list.Connect().SubscribeSafe(observer), list, Disposable.Create(() => - { - observer.OnCompleted(); - disposeAction?.Dispose(); - })); - }); + return Observable.Create>( + observer => + { + var list = new SourceList(); + IDisposable? disposeAction = null; + + try + { + disposeAction = subscribe(list); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable( + list.Connect().SubscribeSafe(observer), + list, + Disposable.Create( + () => + { + observer.OnCompleted(); + disposeAction?.Dispose(); + })); + }); } /// /// Creates an observable list from a specified Subscribe method implementation. /// - /// The type of the elements contained in the observable list + /// The type of the elements contained in the observable list. /// Implementation of the resulting observable list's Subscribe method. /// The observable list with the specified implementation for the Subscribe method. public static IObservable> Create(Func, Task> subscribe) { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - return Create((list, ct) => subscribe(list)); + return Create((list, _) => subscribe(list)); } /// /// Creates an observable list from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation. /// - /// The type of the elements contained in the observable list + /// The type of the elements contained in the observable list. /// Implementation of the resulting observable list's Subscribe method. /// The observable list with the specified implementation for the Subscribe method. public static IObservable> Create(Func, CancellationToken, Task> subscribe) { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - return Observable.Create>(async (observer, ct) => - { - var list = new SourceList(); - IDisposable disposeAction = null; - SingleAssignmentDisposable actionDisposable = new SingleAssignmentDisposable(); - - try - { - disposeAction = await subscribe(list, ct).ConfigureAwait(false); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(list.Connect().SubscribeSafe(observer), list, actionDisposable, Disposable.Create(() => - { - observer.OnCompleted(); - disposeAction?.Dispose(); - })); - }); + return Observable.Create>( + async (observer, ct) => + { + var list = new SourceList(); + IDisposable? disposeAction = null; + SingleAssignmentDisposable actionDisposable = new(); + + try + { + disposeAction = await subscribe(list, ct).ConfigureAwait(false); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable( + list.Connect().SubscribeSafe(observer), + list, + actionDisposable, + Disposable.Create( + () => + { + observer.OnCompleted(); + disposeAction?.Dispose(); + })); + }); } /// /// Creates an observable list from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation. /// - /// The type of the elements contained in the observable list + /// The type of the elements contained in the observable list. /// Implementation of the resulting observable list's Subscribe method. /// The observable list with the specified implementation for the Subscribe method. - public static IObservable> Create(Func, Task> subscribe) + public static IObservable> Create(Func, Task> subscribe) { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - return Create(async (list, ct) => await subscribe(list).ConfigureAwait(false)); + return Create(async (list, _) => await subscribe(list).ConfigureAwait(false)); } /// /// Creates an observable list from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation. /// - /// The type of the elements contained in the observable list + /// The type of the elements contained in the observable list. /// Implementation of the resulting observable list's Subscribe method. /// The observable list with the specified implementation for the Subscribe method. public static IObservable> Create(Func, CancellationToken, Task> subscribe) { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - return Observable.Create>(async (observer, ct) => - { - var list = new SourceList(); - Action disposeAction = null; - - try - { - disposeAction = await subscribe(list, ct).ConfigureAwait(false); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(list.Connect().SubscribeSafe(observer), list, Disposable.Create(() => - { - observer.OnCompleted(); - disposeAction?.Invoke(); - })); - }); + return Observable.Create>( + async (observer, ct) => + { + var list = new SourceList(); + Action? disposeAction = null; + + try + { + disposeAction = await subscribe(list, ct).ConfigureAwait(false); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable( + list.Connect().SubscribeSafe(observer), + list, + Disposable.Create( + () => + { + observer.OnCompleted(); + disposeAction?.Invoke(); + })); + }); } /// /// Creates an observable list from a specified asynchronous Subscribe method. /// - /// The type of the elements contained in the observable list + /// The type of the elements contained in the observable list. /// Implementation of the resulting observable list's Subscribe method. /// The observable list with the specified implementation for the Subscribe method. public static IObservable> Create(Func, Task> subscribe) { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - return Observable.Create>(async observer => - { - var list = new SourceList(); - - try - { - await subscribe(list).ConfigureAwait(false); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(list.Connect().SubscribeSafe(observer), list, Disposable.Create(observer.OnCompleted)); - }); + return Observable.Create>( + async observer => + { + var list = new SourceList(); + + try + { + await subscribe(list).ConfigureAwait(false); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable(list.Connect().SubscribeSafe(observer), list, Disposable.Create(observer.OnCompleted)); + }); } /// /// Creates an observable list from a specified cancellable asynchronous Subscribe method. The CancellationToken passed to the asynchronous Subscribe method is tied to the returned disposable subscription, allowing best-effort cancellation. /// - /// The type of the elements contained in the observable list + /// The type of the elements contained in the observable list. /// Implementation of the resulting observable list's Subscribe method. /// The observable list with the specified implementation for the Subscribe method. public static IObservable> Create(Func, CancellationToken, Task> subscribe) { - if (subscribe == null) + if (subscribe is null) { throw new ArgumentNullException(nameof(subscribe)); } - return Observable.Create>(async (observer, ct) => - { - var list = new SourceList(); - - try - { - await subscribe(list,ct).ConfigureAwait(false); - } - catch (Exception e) - { - observer.OnError(e); - } - - return new CompositeDisposable(list.Connect().SubscribeSafe(observer), list, Disposable.Create(observer.OnCompleted)); - }); + return Observable.Create>( + async (observer, ct) => + { + var list = new SourceList(); + + try + { + await subscribe(list, ct).ConfigureAwait(false); + } + catch (Exception e) + { + observer.OnError(e); + } + + return new CompositeDisposable(list.Connect().SubscribeSafe(observer), list, Disposable.Create(observer.OnCompleted)); + }); } - - #endregion } } \ No newline at end of file diff --git a/src/DynamicData/ObsoleteEx.cs b/src/DynamicData/ObsoleteEx.cs index eee6b93b7..e5bbdb040 100644 --- a/src/DynamicData/ObsoleteEx.cs +++ b/src/DynamicData/ObsoleteEx.cs @@ -1,19 +1,13 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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 { - internal static class Constants - { - public const string EvaluateIsDead = "Use Refresh: Same thing but better semantics"; - } - /// - /// Obsolete methods: Kept in system to prevent breaking changes for now + /// Obsolete methods: Kept in system to prevent breaking changes for now. /// public static class ObsoleteEx { - } -} +} \ No newline at end of file diff --git a/src/DynamicData/Platforms/net45/PLinqFilteredUpdater.cs b/src/DynamicData/Platforms/net45/PFilter.cs similarity index 76% rename from src/DynamicData/Platforms/net45/PLinqFilteredUpdater.cs rename to src/DynamicData/Platforms/net45/PFilter.cs index bf3c8a77a..3d50dd063 100644 --- a/src/DynamicData/Platforms/net45/PLinqFilteredUpdater.cs +++ b/src/DynamicData/Platforms/net45/PFilter.cs @@ -1,13 +1,13 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. #if P_LINQ - using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; + using DynamicData.Cache.Internal; using DynamicData.Kernel; @@ -15,11 +15,14 @@ namespace DynamicData.PLinq { internal class PFilter + where TKey : notnull { - private readonly IObservable> _source; private readonly Func _filter; + private readonly ParallelisationOptions _parallelisationOptions; + private readonly IObservable> _source; + public PFilter(IObservable> source, Func filter, ParallelisationOptions parallelisationOptions) { _source = source; @@ -29,17 +32,15 @@ public PFilter(IObservable> source, Func> Run() { - return Observable.Create>(observer => - { - var filterer = new PLinqFilteredUpdater(_filter, _parallelisationOptions); - return _source - .Select(filterer.Update) - .NotEmpty() - .SubscribeSafe(observer); - }); + return Observable.Create>( + observer => + { + var filterer = new PLinqFilteredUpdater(_filter, _parallelisationOptions); + return _source.Select(filterer.Update).NotEmpty().SubscribeSafe(observer); + }); } - private class PLinqFilteredUpdater: AbstractFilter + private class PLinqFilteredUpdater : AbstractFilter { private readonly ParallelisationOptions _parallelisationOptions; @@ -49,27 +50,24 @@ public PLinqFilteredUpdater(Func filter, ParallelisationOptions p _parallelisationOptions = parallelisationOptions; } - protected override IEnumerable> Refresh(IEnumerable> items, Func, Optional>> factory) - { - var keyValuePairs = items as KeyValuePair[] ?? items.ToArray(); - - return keyValuePairs.ShouldParallelise(_parallelisationOptions) - ? keyValuePairs.Parallelise(_parallelisationOptions).Select(factory).SelectValues() - : keyValuePairs.Select(factory).SelectValues(); - } - protected override IEnumerable GetChangesWithFilter(IChangeSet updates) { if (updates.ShouldParallelise(_parallelisationOptions)) { - return updates.Parallelise(_parallelisationOptions) - .Select(u => new UpdateWithFilter(Filter(u.Current), u)).ToArray(); + return updates.Parallelise(_parallelisationOptions).Select(u => new UpdateWithFilter(Filter(u.Current), u)).ToArray(); } return updates.Select(u => new UpdateWithFilter(Filter(u.Current), u)).ToArray(); } + + protected override IEnumerable> Refresh(IEnumerable> items, Func, Optional>> factory) + { + var keyValuePairs = items as KeyValuePair[] ?? items.ToArray(); + + return keyValuePairs.ShouldParallelise(_parallelisationOptions) ? keyValuePairs.Parallelise(_parallelisationOptions).Select(factory).SelectValues() : keyValuePairs.Select(factory).SelectValues(); + } } } - } -#endif + +#endif \ No newline at end of file diff --git a/src/DynamicData/Platforms/net45/PSubscribeMany.cs b/src/DynamicData/Platforms/net45/PSubscribeMany.cs index 4d1e0f2ad..29490063a 100644 --- a/src/DynamicData/Platforms/net45/PSubscribeMany.cs +++ b/src/DynamicData/Platforms/net45/PSubscribeMany.cs @@ -1,9 +1,8 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. #if P_LINQ - using System; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -12,14 +11,16 @@ namespace DynamicData.PLinq { internal class PSubscribeMany + where TKey : notnull { private readonly ParallelisationOptions _parallelisationOptions; + private readonly IObservable> _source; + private readonly Func _subscriptionFactory; public PSubscribeMany(IObservable> source, Func subscriptionFactory, ParallelisationOptions parallelisationOptions) { - _source = source ?? throw new ArgumentNullException(nameof(source)); _subscriptionFactory = subscriptionFactory ?? throw new ArgumentNullException(nameof(subscriptionFactory)); _parallelisationOptions = parallelisationOptions; @@ -27,23 +28,16 @@ public PSubscribeMany(IObservable> source, Func> Run() { - - return Observable.Create> - ( + return Observable.Create>( observer => - { - var published = _source.Publish(); - var subscriptions = published - .Transform((t, k) => _subscriptionFactory(t, k), _parallelisationOptions) - .DisposeMany() - .Subscribe(); - - return new CompositeDisposable( - subscriptions, - published.SubscribeSafe(observer), - published.Connect()); - }); + { + var published = _source.Publish(); + var subscriptions = published.Transform((t, k) => _subscriptionFactory(t, k), _parallelisationOptions).DisposeMany().Subscribe(); + + return new CompositeDisposable(subscriptions, published.SubscribeSafe(observer), published.Connect()); + }); } } } + #endif \ No newline at end of file diff --git a/src/DynamicData/Platforms/net45/PTransform.cs b/src/DynamicData/Platforms/net45/PTransform.cs index 835121013..ec715e2e5 100644 --- a/src/DynamicData/Platforms/net45/PTransform.cs +++ b/src/DynamicData/Platforms/net45/PTransform.cs @@ -1,29 +1,31 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. #if P_LINQ - using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; + using DynamicData.Kernel; // ReSharper disable once CheckNamespace namespace DynamicData.PLinq { internal sealed class PTransform + where TKey : notnull { private readonly IObservable> _source; private readonly Func, TKey, TDestination> _transformFactory; private readonly ParallelisationOptions _parallelisationOptions; - private readonly Action> _exceptionCallback; + private readonly Action>? _exceptionCallback; - public PTransform(IObservable> source, + public PTransform( + IObservable> source, Func, TKey, TDestination> transformFactory, ParallelisationOptions parallelisationOptions, - Action> exceptionCallback = null) + Action>? exceptionCallback = null) { _source = source; _exceptionCallback = exceptionCallback; @@ -43,8 +45,6 @@ public IObservable> Run() private IChangeSet DoTransform(ChangeAwareCache cache, IChangeSet changes) { - // var transformed = changes.Select(ToDestination); - var transformed = changes.ShouldParallelise(_parallelisationOptions) ? changes.Parallelise(_parallelisationOptions).Select(ToDestination).ToArray() : changes.Select(ToDestination).ToArray(); @@ -66,7 +66,7 @@ private TransformResult ToDestination(Change change) } catch (Exception ex) { - //only handle errors if a handler has been specified + // only handle errors if a handler has been specified if (_exceptionCallback != null) { return new TransformResult(change, ex); @@ -86,7 +86,9 @@ private IChangeSet ProcessUpdates(ChangeAwareCache ProcessUpdates(ChangeAwareCache(result.Error, result.Change.Current, result.Change.Key)); + _exceptionCallback?.Invoke(new Error(result.Error, result.Change.Current, result.Change.Key)); } } return cache.CaptureChanges(); } - private struct TransformResult + private readonly struct TransformResult { - public Change Change { get; } - public Exception Error { get; } - public bool Success { get; } - public Optional Destination { get; } - public TKey Key { get; } - public TransformResult(Change change, TDestination destination) : this() { @@ -141,6 +137,16 @@ public TransformResult(Change change, Exception error) Success = false; Key = change.Key; } + + public Change Change { get; } + + public Exception? Error { get; } + + public bool Success { get; } + + public Optional Destination { get; } + + public TKey Key { get; } } } } diff --git a/src/DynamicData/Platforms/net45/ParallelEx.cs b/src/DynamicData/Platforms/net45/ParallelEx.cs index c7ad428e3..bdb1c3efc 100644 --- a/src/DynamicData/Platforms/net45/ParallelEx.cs +++ b/src/DynamicData/Platforms/net45/ParallelEx.cs @@ -1,9 +1,8 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. #if P_LINQ - using System; using System.Collections.Generic; using System.Linq; @@ -12,17 +11,12 @@ namespace DynamicData.PLinq { /// - /// Parallelisation extensions for DynamicData + /// Parallelisation extensions for DynamicData. /// internal static class ParallelEx { - internal static bool ShouldParallelise(this IChangeSet source, ParallelisationOptions option) - { - return (option.Type == ParallelType.Parallelise || option.Type == ParallelType.Ordered) - && (option.Threshold >= 0 && source.Count >= option.Threshold); - } - internal static ParallelQuery> Parallelise(this IChangeSet source, ParallelisationOptions option) + where TKey : notnull { switch (option.Type) { @@ -31,18 +25,14 @@ internal static ParallelQuery> Parallelise( case ParallelType.Ordered: return source.AsParallel().AsOrdered(); + default: throw new ArgumentException("Should not parallelise! Call ShouldParallelise() first"); } } - internal static bool ShouldParallelise(this IEnumerable> source, ParallelisationOptions option) - { - return (option.Type == ParallelType.Parallelise || option.Type == ParallelType.Ordered) - && (option.Threshold >= 0 && source.Skip(option.Threshold).Any()); - } - internal static ParallelQuery> Parallelise(this IEnumerable> source, ParallelisationOptions option) + where TKey : notnull { switch (option.Type) { @@ -51,6 +41,7 @@ internal static ParallelQuery> Parallelise Parallelise(this IEnumerable source, Parall return source; } } + + internal static bool ShouldParallelise(this IChangeSet source, ParallelisationOptions option) + where TKey : notnull + { + return (option.Type == ParallelType.Parallelise || option.Type == ParallelType.Ordered) && (option.Threshold >= 0 && source.Count >= option.Threshold); + } + + internal static bool ShouldParallelise(this IEnumerable> source, ParallelisationOptions option) + where TKey : notnull + { + return (option.Type == ParallelType.Parallelise || option.Type == ParallelType.Ordered) && (option.Threshold >= 0 && source.Skip(option.Threshold).Any()); + } } } - -#endif +#endif \ No newline at end of file diff --git a/src/DynamicData/Platforms/net45/ParallelOperators.cs b/src/DynamicData/Platforms/net45/ParallelOperators.cs index aa16dcdf8..729b0cc13 100644 --- a/src/DynamicData/Platforms/net45/ParallelOperators.cs +++ b/src/DynamicData/Platforms/net45/ParallelOperators.cs @@ -1,89 +1,106 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. #if P_LINQ - using System; + using DynamicData.Kernel; // ReSharper disable once CheckNamespace - namespace DynamicData.PLinq { /// - /// PLinq operators or Net4 and Net45 only + /// PLinq operators or Net4 and Net45 only. /// public static class ParallelOperators { - #region Subscribe Many + /// + /// Filters the stream using the specified predicate. + /// + /// The type of the object. + /// The type of the key. + /// The source. + /// The filter. + /// The parallelisation options. + /// An observable which emits a change set. + /// source. + public static IObservable> Filter(this IObservable> source, Func filter, ParallelisationOptions parallelisationOptions) + where TKey : notnull + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + return new PFilter(source, filter, parallelisationOptions).Run(); + } /// - /// Subscribes to each item when it is added to the stream and unsubcribes when it is removed. All items will be unsubscribed when the stream is disposed + /// Subscribes to each item when it is added to the stream and unsubscribes when it is removed. All items will be unsubscribed when the stream is disposed. /// /// The type of the object. /// The type of the key. /// The source. - /// The subsription function + /// The subscription function. /// The parallelisation options. - /// + /// An observable which emits a change set. /// source /// or - /// subscriptionFactory + /// subscriptionFactory. /// - /// Subscribes to each item when it is added or updates and unsubcribes when it is removed + /// Subscribes to each item when it is added or updates and unsubscribes when it is removed. /// - public static IObservable> SubscribeMany(this IObservable> source, - Func subscriptionFactory, - ParallelisationOptions parallelisationOptions) + public static IObservable> SubscribeMany(this IObservable> source, Func subscriptionFactory, ParallelisationOptions parallelisationOptions) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (subscriptionFactory == null) + if (subscriptionFactory is null) { throw new ArgumentNullException(nameof(subscriptionFactory)); } - if (parallelisationOptions == null) + if (parallelisationOptions is null) { throw new ArgumentNullException(nameof(parallelisationOptions)); } - return new PSubscribeMany(source,(t,v)=> subscriptionFactory(t),parallelisationOptions).Run(); + return new PSubscribeMany(source, (t, _) => subscriptionFactory(t), parallelisationOptions).Run(); } /// - /// Subscribes to each item when it is added to the stream and unsubcribes when it is removed. All items will be unsubscribed when the stream is disposed + /// Subscribes to each item when it is added to the stream and unsubscribes when it is removed. All items will be unsubscribed when the stream is disposed. /// /// The type of the object. /// The type of the key. /// The source. - /// The subsription function + /// The subscription function. /// The parallelisation options. - /// + /// An observable which emits a change set. /// source /// or - /// subscriptionFactory + /// subscriptionFactory. /// - /// Subscribes to each item when it is added or updates and unsubcribes when it is removed + /// Subscribes to each item when it is added or updates and unsubscribes when it is removed. /// - public static IObservable> SubscribeMany(this IObservable> source, - Func subscriptionFactory, ParallelisationOptions parallelisationOptions) + public static IObservable> SubscribeMany(this IObservable> source, Func subscriptionFactory, ParallelisationOptions parallelisationOptions) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (subscriptionFactory == null) + if (subscriptionFactory is null) { throw new ArgumentNullException(nameof(subscriptionFactory)); } - if (parallelisationOptions == null) + if (parallelisationOptions is null) { throw new ArgumentNullException(nameof(parallelisationOptions)); } @@ -91,49 +108,44 @@ public static IObservable> SubscribeMany(source, subscriptionFactory, parallelisationOptions).Run(); } - #endregion - - #region Transform - /// - /// Projects each update item to a new form using the specified transform function + /// Projects each update item to a new form using the specified transform function. /// /// The type of the destination. /// The type of the source. /// The type of the key. /// The source. /// The transform factory. - /// The parallelisation options to be used on the transforms + /// The parallelisation options to be used on the transforms. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, - Func transformFactory, - ParallelisationOptions parallelisationOptions) + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func transformFactory, ParallelisationOptions parallelisationOptions) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (transformFactory is null) { throw new ArgumentNullException(nameof(transformFactory)); } - if (parallelisationOptions == null) + if (parallelisationOptions is null) { throw new ArgumentNullException(nameof(parallelisationOptions)); } - return new PTransform(source, (t, p, k) => transformFactory(t, k), parallelisationOptions).Run(); + return new PTransform(source, (t, _, k) => transformFactory(t, k), parallelisationOptions).Run(); } /// - /// Projects each update item to a new form using the specified transform function + /// Projects each update item to a new form using the specified transform function. /// /// The type of the destination. /// The type of the source. @@ -142,16 +154,15 @@ public static IObservable> TransformThe transform factory. /// The parallelisation options. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> Transform(this IObservable> source, - Func transformFactory, - ParallelisationOptions parallelisationOptions) + /// transformFactory. + public static IObservable> Transform(this IObservable> source, Func transformFactory, ParallelisationOptions parallelisationOptions) + where TKey : notnull { - return new PTransform(source, (t, p, k) => transformFactory(t), parallelisationOptions).Run(); + return new PTransform(source, (t, _, _) => transformFactory(t), parallelisationOptions).Run(); } /// @@ -168,37 +179,35 @@ public static IObservable> Transform /// The parallelisation options. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> TransformSafe(this IObservable> source, - Func transformFactory, - Action> errorHandler, - ParallelisationOptions parallelisationOptions) + /// transformFactory. + public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, ParallelisationOptions parallelisationOptions) + where TKey : notnull { - if (source == null) + if (source is null) { throw new ArgumentNullException(nameof(source)); } - if (transformFactory == null) + if (transformFactory is null) { throw new ArgumentNullException(nameof(transformFactory)); } - if (errorHandler == null) + if (errorHandler is null) { throw new ArgumentNullException(nameof(errorHandler)); } - if (parallelisationOptions == null) + if (parallelisationOptions is null) { throw new ArgumentNullException(nameof(parallelisationOptions)); } - return new PTransform(source, (t, p, k) => transformFactory(t), parallelisationOptions, errorHandler).Run(); + return new PTransform(source, (t, _, _) => transformFactory(t), parallelisationOptions, errorHandler).Run(); } /// @@ -213,51 +222,18 @@ public static IObservable> TransformSafeProvides the option to safely handle errors without killing the stream. /// If not specified the stream will terminate as per rx convention. /// - /// The parallelisation options to be used on the transforms + /// The parallelisation options to be used on the transforms. /// - /// A transformed update collection + /// A transformed update collection. /// /// source /// or - /// transformFactory - public static IObservable> TransformSafe(this IObservable> source, - Func transformFactory, - Action> errorHandler, - ParallelisationOptions parallelisationOptions) - { - return new PTransform(source, (t, p, k) => transformFactory(t, k), parallelisationOptions, errorHandler).Run(); - } - - #endregion - - #region Filter - - /// - /// Filters the stream using the specified predicate - /// - /// The type of the object. - /// The type of the key. - /// The source. - /// The filter. - /// The parallelisation options. - /// - /// source - public static IObservable> Filter(this IObservable> source, Func filter, ParallelisationOptions parallelisationOptions) + /// transformFactory. + public static IObservable> TransformSafe(this IObservable> source, Func transformFactory, Action> errorHandler, ParallelisationOptions parallelisationOptions) + where TKey : notnull { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (filter == null) - { - return source; - } - - return new PFilter(source, filter, parallelisationOptions).Run(); + return new PTransform(source, (t, _, k) => transformFactory(t, k), parallelisationOptions, errorHandler).Run(); } - - #endregion } } diff --git a/src/DynamicData/Platforms/net45/ParallelType.cs b/src/DynamicData/Platforms/net45/ParallelType.cs index 105bc2f9b..855795d30 100644 --- a/src/DynamicData/Platforms/net45/ParallelType.cs +++ b/src/DynamicData/Platforms/net45/ParallelType.cs @@ -1,27 +1,31 @@ -#if P_LINQ +// Copyright (c) 2011-2020 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. +#if P_LINQ // ReSharper disable once CheckNamespace namespace DynamicData.PLinq { /// - /// The type of parallisation + /// The type of parallelisation. /// public enum ParallelType { /// - /// No parallelisation will take place + /// No parallelisation will take place. /// None, /// - /// Parallelisation will take place without preserving the enumerable order + /// Parallelisation will take place without preserving the enumerable order. /// Parallelise, /// - /// Parallelisation will take place whilst preserving the enumerable order + /// Parallelisation will take place whilst preserving the enumerable order. /// Ordered } } -#endif + +#endif \ No newline at end of file diff --git a/src/DynamicData/Platforms/net45/ParallelisationOptions.cs b/src/DynamicData/Platforms/net45/ParallelisationOptions.cs index 191b4850f..37a6dfb68 100644 --- a/src/DynamicData/Platforms/net45/ParallelisationOptions.cs +++ b/src/DynamicData/Platforms/net45/ParallelisationOptions.cs @@ -1,9 +1,7 @@ -// Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. +// Copyright (c) 2011-2020 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. -using System.Diagnostics.CodeAnalysis; - #if P_LINQ // ReSharper disable once CheckNamespace namespace DynamicData.PLinq @@ -11,33 +9,35 @@ namespace DynamicData.PLinq /// /// Options to specify parallelisation of stream operations. Only applicable for .Net4 and .Net45 builds. /// - [SuppressMessage("ReSharper", "CommentTypo")] public class ParallelisationOptions { /// - /// The default parallelisation options + /// The default parallelisation options. /// - public static readonly ParallelisationOptions Default = new ParallelisationOptions(ParallelType.Ordered, 0); + public static readonly ParallelisationOptions Default = new(ParallelType.Ordered); /// - /// Value to be used when no parallelisation should take place + /// Value to be used when no parallelisation should take place. /// - public static readonly ParallelisationOptions None = new ParallelisationOptions(ParallelType.None, 0); + public static readonly ParallelisationOptions None = new(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public ParallelisationOptions(ParallelType type = ParallelType.None, int threshold = 0, int maxDegreeOfParallisation = 0) + /// The type of parallel operation. + /// The threshold before making the operation parallel. + /// The maximum degrees of parallelism. + public ParallelisationOptions(ParallelType type = ParallelType.None, int threshold = 0, int maxDegreeOfParallelisation = 0) { Type = type; Threshold = threshold; - MaxDegreeOfParallisation = maxDegreeOfParallisation; + MaxDegreeOfParallelisation = maxDegreeOfParallelisation; } /// - /// Gets the type. + /// Gets the maximum degree of parallelisation. /// - public ParallelType Type { get; } + public int MaxDegreeOfParallelisation { get; } /// /// Gets the threshold. @@ -45,9 +45,10 @@ public ParallelisationOptions(ParallelType type = ParallelType.None, int thresho public int Threshold { get; } /// - /// Gets the maximum degree of parallisation. + /// Gets the type. /// - public int MaxDegreeOfParallisation { get; } + public ParallelType Type { get; } } } -#endif + +#endif \ No newline at end of file diff --git a/src/DynamicData/Properties/Annotations.cs b/src/DynamicData/Properties/Annotations.cs deleted file mode 100644 index 86aeaa0b9..000000000 --- a/src/DynamicData/Properties/Annotations.cs +++ /dev/null @@ -1,1060 +0,0 @@ -// Copyright (c) 2011-2019 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. - -// -using System; -using System.Diagnostics; - -#pragma warning disable 1591 -// ReSharper disable UnusedMember.Global -// ReSharper disable MemberCanBePrivate.Global -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable IntroduceOptionalParameters.Global -// ReSharper disable MemberCanBeProtected.Global -// ReSharper disable InconsistentNaming - -// ReSharper disable once CheckNamespace - -namespace DynamicData.Annotations -{ - /// - /// Indicates that the value of the marked element could be null sometimes, - /// so the check for null is necessary before its usage - /// - /// - /// [CanBeNull] public object Test() { return null; } - /// public void UseTest() { - /// var p = Test(); - /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' - /// } - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class CanBeNullAttribute : Attribute - { - } - - /// - /// Indicates that the value of the marked element could never be null - /// - /// - /// [NotNull] public object Foo() { - /// return null; // Warning: Possible 'null' assignment - /// } - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class NotNullAttribute : Attribute - { - } - - /// - /// Indicates that collection or enumerable value does not contain null elements - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class ItemNotNullAttribute : Attribute - { - } - - /// - /// Indicates that collection or enumerable value can contain null elements - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class ItemCanBeNullAttribute : Attribute - { - } - - /// - /// Indicates that the marked method builds string by format pattern and (optional) arguments. - /// Parameter, which contains format string, should be given in constructor. The format string - /// should be in -like form - /// - /// - /// [StringFormatMethod("message")] - /// public void ShowError(string message, params object[] args) { /* do something */ } - /// public void Foo() { - /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string - /// } - /// - [AttributeUsage( - AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Delegate)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class StringFormatMethodAttribute : Attribute - { - /// - /// Specifies which parameter of an annotated method should be treated as format-string - /// - public StringFormatMethodAttribute(string formatParameterName) - { - FormatParameterName = formatParameterName; - } - - public string FormatParameterName { get; private set; } - } - - /// - /// For a parameter that is expected to be one of the limited set of values. - /// Specify fields of which type should be used as values for this parameter. - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class ValueProviderAttribute : Attribute - { - public ValueProviderAttribute(string name) - { - Name = name; - } - - [NotNull] - public string Name { get; private set; } - } - - /// - /// Indicates that the function argument should be string literal and match one - /// of the parameters of the caller function. For example, ReSharper annotates - /// the parameter of - /// - /// - /// public void Foo(string param) { - /// if (param == null) - /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol - /// } - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class InvokerParameterNameAttribute : Attribute - { - } - - /// - /// Indicates that the method is contained in a type that implements - /// System.ComponentModel.INotifyPropertyChanged interface and this method - /// is used to notify that some property value changed - /// - /// - /// The method should be non-static and conform to one of the supported signatures: - /// - /// NotifyChanged(string) - /// NotifyChanged(params string[]) - /// NotifyChanged{T}(Expression{Func{T}}) - /// NotifyChanged{T,U}(Expression{Func{T,U}}) - /// SetProperty{T}(ref T, T, string) - /// - /// - /// - /// public class Foo : INotifyPropertyChanged { - /// public event PropertyChangedEventHandler PropertyChanged; - /// [NotifyPropertyChangedInvocator] - /// protected virtual void NotifyChanged(string propertyName) { ... } - /// - /// private string _name; - /// public string Name { - /// get { return _name; } - /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } - /// } - /// } - /// - /// Examples of generated notifications: - /// - /// NotifyChanged("Property") - /// NotifyChanged(() => Property) - /// NotifyChanged((VM x) => x.Property) - /// SetProperty(ref myField, value, "Property") - /// - /// - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute - { - public NotifyPropertyChangedInvocatorAttribute() - { - } - - public NotifyPropertyChangedInvocatorAttribute(string parameterName) - { - ParameterName = parameterName; - } - - public string ParameterName { get; private set; } - } - - /// - /// Describes dependency between method input and output - /// - /// - ///

Function Definition Table syntax:

- /// - /// FDT ::= FDTRow [;FDTRow]* - /// FDTRow ::= Input => Output | Output <= Input - /// Input ::= ParameterName: Value [, Input]* - /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} - /// Value ::= true | false | null | notnull | canbenull - /// - /// If method has single input parameter, it's name could be omitted.
- /// Using halt (or void/nothing, which is the same) - /// for method output means that the methods doesn't return normally.
- /// canbenull annotation is only applicable for output parameters.
- /// You can use multiple [ContractAnnotation] for each FDT row, - /// or use single attribute with rows separated by semicolon.
- ///
- /// - /// - /// [ContractAnnotation("=> halt")] - /// public void TerminationMethod() - /// - /// - /// [ContractAnnotation("halt <= condition: false")] - /// public void Assert(bool condition, string text) // regular assertion method - /// - /// - /// [ContractAnnotation("s:null => true")] - /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() - /// - /// - /// // A method that returns null if the parameter is null, - /// // and not null if the parameter is not null - /// [ContractAnnotation("null => null; notnull => notnull")] - /// public object Transform(object data) - /// - /// - /// [ContractAnnotation("s:null=>false; =>true,result:notnull; =>false, result:null")] - /// public bool TryParse(string s, out Person result) - /// - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class ContractAnnotationAttribute : Attribute - { - public ContractAnnotationAttribute([NotNull] string contract) - : this(contract, false) - { - } - - public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) - { - Contract = contract; - ForceFullStates = forceFullStates; - } - - public string Contract { get; private set; } - public bool ForceFullStates { get; private set; } - } - - /// - /// Indicates that marked element should be localized or not - /// - /// - /// [LocalizationRequiredAttribute(true)] - /// public class Foo { - /// private string str = "my string"; // Warning: Localizable string - /// } - /// - [AttributeUsage(AttributeTargets.All)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class LocalizationRequiredAttribute : Attribute - { - public LocalizationRequiredAttribute() - : this(true) - { - } - - public LocalizationRequiredAttribute(bool required) - { - Required = required; - } - - public bool Required { get; private set; } - } - - /// - /// Indicates that the value of the marked type (or its derivatives) - /// cannot be compared using '==' or '!=' operators and Equals() - /// should be used instead. However, using '==' or '!=' for comparison - /// with null is always permitted. - /// - /// - /// [CannotApplyEqualityOperator] - /// class NoEquality { } - /// class UsesNoEquality { - /// public void Test() { - /// var ca1 = new NoEquality(); - /// var ca2 = new NoEquality(); - /// if (ca1 != null) { // OK - /// bool condition = ca1 == ca2; // Warning - /// } - /// } - /// } - /// - [AttributeUsage( - AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class CannotApplyEqualityOperatorAttribute : Attribute - { - } - - /// - /// When applied to a target attribute, specifies a requirement for any type marked - /// with the target attribute to implement or inherit specific type or types. - /// - /// - /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement - /// public class ComponentAttribute : Attribute { } - /// [Component] // ComponentAttribute requires implementing IComponent interface - /// public class MyComponent : IComponent { } - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - [BaseTypeRequired(typeof(Attribute))] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class BaseTypeRequiredAttribute : Attribute - { - public BaseTypeRequiredAttribute([NotNull] Type baseType) - { - BaseType = baseType; - } - - [NotNull] - public Type BaseType { get; private set; } - } - - /// - /// Indicates that the marked symbol is used implicitly - /// (e.g. via reflection, in external library), so this symbol - /// will not be marked as unused (as well as by other usage inspections) - /// - [AttributeUsage(AttributeTargets.All)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class UsedImplicitlyAttribute : Attribute - { - public UsedImplicitlyAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) - { - } - - public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) - { - } - - public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) - { - } - - public UsedImplicitlyAttribute( - ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) - { - UseKindFlags = useKindFlags; - TargetFlags = targetFlags; - } - - public ImplicitUseKindFlags UseKindFlags { get; private set; } - public ImplicitUseTargetFlags TargetFlags { get; private set; } - } - - /// - /// Should be used on attributes and causes ReSharper - /// to not mark symbols marked with such attributes as unused - /// (as well as by other usage inspections) - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class MeansImplicitUseAttribute : Attribute - { - public MeansImplicitUseAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) - { - } - - public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) - { - } - - public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) - { - } - - public MeansImplicitUseAttribute( - ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) - { - UseKindFlags = useKindFlags; - TargetFlags = targetFlags; - } - - [UsedImplicitly] - public ImplicitUseKindFlags UseKindFlags { get; private set; } - - [UsedImplicitly] - public ImplicitUseTargetFlags TargetFlags { get; private set; } - } - - [Flags] - public enum ImplicitUseKindFlags - { - Default = Access | Assign | InstantiatedWithFixedConstructorSignature, - - /// Only entity marked with attribute considered used - Access = 1, - - /// Indicates implicit assignment to a member - Assign = 2, - - /// - /// Indicates implicit instantiation of a type with fixed constructor signature. - /// That means any unused constructor parameters won't be reported as such. - /// - InstantiatedWithFixedConstructorSignature = 4, - - /// Indicates implicit instantiation of a type - InstantiatedNoFixedConstructorSignature = 8, - } - - /// - /// Specify what is considered used implicitly when marked - /// with or - /// - [Flags] - public enum ImplicitUseTargetFlags - { - Default = Itself, - Itself = 1, - - /// Members of entity marked with attribute are considered used - Members = 2, - - /// Entity marked with attribute and all its members considered used - WithMembers = Itself | Members - } - - /// - /// This attribute is intended to mark publicly available API - /// which should not be removed and so is treated as used - /// - [MeansImplicitUse] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class PublicAPIAttribute : Attribute - { - public PublicAPIAttribute() - { - } - - public PublicAPIAttribute([NotNull] string comment) - { - Comment = comment; - } - - public string Comment { get; private set; } - } - - /// - /// Tells code analysis engine if the parameter is completely handled - /// when the invoked method is on stack. If the parameter is a delegate, - /// indicates that delegate is executed while the method is executed. - /// If the parameter is an enumerable, indicates that it is enumerated - /// while the method is executed - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class InstantHandleAttribute : Attribute - { - } - - /// - /// Indicates that a method does not make any observable state changes. - /// The same as System.Diagnostics.Contracts.PureAttribute - /// - /// - /// [Pure] private int Multiply(int x, int y) { return x * y; } - /// public void Foo() { - /// const int a = 2, b = 2; - /// Multiply(a, b); // Waring: Return value of pure method is not used - /// } - /// - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class PureAttribute : Attribute - { - } - - /// - /// Indicates that a parameter is a path to a file or a folder within a web project. - /// Path can be relative or absolute, starting from web root (~) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public class PathReferenceAttribute : Attribute - { - public PathReferenceAttribute() - { - } - - public PathReferenceAttribute([PathReference] string basePath) - { - BasePath = basePath; - } - - public string BasePath { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute - { - public AspMvcAreaMasterLocationFormatAttribute(string format) - { - Format = format; - } - - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute - { - public AspMvcAreaPartialViewLocationFormatAttribute(string format) - { - Format = format; - } - - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcAreaViewLocationFormatAttribute : Attribute - { - public AspMvcAreaViewLocationFormatAttribute(string format) - { - Format = format; - } - - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcMasterLocationFormatAttribute : Attribute - { - public AspMvcMasterLocationFormatAttribute(string format) - { - Format = format; - } - - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcPartialViewLocationFormatAttribute : Attribute - { - public AspMvcPartialViewLocationFormatAttribute(string format) - { - Format = format; - } - - public string Format { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcViewLocationFormatAttribute : Attribute - { - public AspMvcViewLocationFormatAttribute(string format) - { - Format = format; - } - - public string Format { get; private set; } - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC action. If applied to a method, the MVC action name is calculated - /// implicitly from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcActionAttribute : Attribute - { - public AspMvcActionAttribute() - { - } - - public AspMvcActionAttribute(string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC area. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcAreaAttribute : PathReferenceAttribute - { - public AspMvcAreaAttribute() - { - } - - public AspMvcAreaAttribute(string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is - /// an MVC controller. If applied to a method, the MVC controller name is calculated - /// implicitly from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcControllerAttribute : Attribute - { - public AspMvcControllerAttribute() - { - } - - public AspMvcControllerAttribute(string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - public string AnonymousProperty { get; private set; } - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC Master. Use this attribute - /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcMasterAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC model type. Use this attribute - /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcModelTypeAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC - /// partial view. If applied to a method, the MVC partial view name is calculated implicitly - /// from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcPartialViewAttribute : PathReferenceAttribute - { - } - - /// - /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcSupressViewErrorAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcDisplayTemplateAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC editor template. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcEditorTemplateAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC template. - /// Use this attribute for custom wrappers similar to - /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String) - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcTemplateAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC view. If applied to a method, the MVC view name is calculated implicitly - /// from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Controller.View(Object) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcViewAttribute : PathReferenceAttribute - { - } - - /// - /// ASP.NET MVC attribute. When applied to a parameter of an attribute, - /// indicates that this parameter is an MVC action name - /// - /// - /// [ActionName("Foo")] - /// public ActionResult Login(string returnUrl) { - /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK - /// return RedirectToAction("Bar"); // Error: Cannot resolve action - /// } - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMvcActionSelectorAttribute : Attribute - { - } - - [AttributeUsage( - AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class HtmlElementAttributesAttribute : Attribute - { - public HtmlElementAttributesAttribute() - { - } - - public HtmlElementAttributesAttribute(string name) - { - Name = name; - } - - public string Name { get; private set; } - } - - [AttributeUsage( - AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class HtmlAttributeValueAttribute : Attribute - { - public HtmlAttributeValueAttribute([NotNull] string name) - { - Name = name; - } - - [NotNull] - public string Name { get; private set; } - } - - /// - /// Razor attribute. Indicates that a parameter or a method is a Razor section. - /// Use this attribute for custom wrappers similar to - /// System.Web.WebPages.WebPageBase.RenderSection(String) - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RazorSectionAttribute : Attribute - { - } - - /// - /// Indicates how method invocation affects content of the collection - /// - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class CollectionAccessAttribute : Attribute - { - public CollectionAccessAttribute(CollectionAccessType collectionAccessType) - { - CollectionAccessType = collectionAccessType; - } - - public CollectionAccessType CollectionAccessType { get; private set; } - } - - [Flags] - public enum CollectionAccessType - { - /// Method does not use or modify content of the collection - None = 0, - - /// Method only reads content of the collection but does not modify it - Read = 1, - - /// Method can change content of the collection but does not add new elements - ModifyExistingContent = 2, - - /// Method can add new elements to the collection - UpdatedContent = ModifyExistingContent | 4 - } - - /// - /// Indicates that the marked method is assertion method, i.e. it halts control flow if - /// one of the conditions is satisfied. To set the condition, mark one of the parameters with - /// attribute - /// - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AssertionMethodAttribute : Attribute - { - } - - /// - /// Indicates the condition parameter of the assertion method. The method itself should be - /// marked by attribute. The mandatory argument of - /// the attribute is the assertion type. - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AssertionConditionAttribute : Attribute - { - public AssertionConditionAttribute(AssertionConditionType conditionType) - { - ConditionType = conditionType; - } - - public AssertionConditionType ConditionType { get; private set; } - } - - /// - /// Specifies assertion type. If the assertion method argument satisfies the condition, - /// then the execution continues. Otherwise, execution is assumed to be halted - /// - public enum AssertionConditionType - { - /// Marked parameter should be evaluated to true - IS_TRUE = 0, - - /// Marked parameter should be evaluated to false - IS_FALSE = 1, - - /// Marked parameter should be evaluated to null value - IS_NULL = 2, - - /// Marked parameter should be evaluated to not null value - IS_NOT_NULL = 3, - } - - /// - /// Indicates that the marked method unconditionally terminates control flow execution. - /// For example, it could unconditionally throw exception - /// - [Obsolete("Use [ContractAnnotation('=> halt')] instead")] - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class TerminatesProgramAttribute : Attribute - { - } - - /// - /// Indicates that method is pure LINQ method, with postponed enumeration (like Enumerable.Select, - /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters - /// of delegate type by analyzing LINQ method chains. - /// - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class LinqTunnelAttribute : Attribute - { - } - - /// - /// Indicates that IEnumerable, passed as parameter, is not enumerated. - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class NoEnumerationAttribute : Attribute - { - } - - /// - /// Indicates that parameter is regular expression pattern. - /// - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RegexPatternAttribute : Attribute - { - } - - /// - /// XAML attribute. Indicates the type that has ItemsSource property and should be - /// treated as ItemsControl-derived type, to enable inner items DataContext - /// type resolve. - /// - [AttributeUsage(AttributeTargets.Class)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class XamlItemsControlAttribute : Attribute - { - } - - /// - /// XAML attibute. Indicates the property of some BindingBase-derived type, that - /// is used to bind some item of ItemsControl-derived type. This annotation will - /// enable the DataContext type resolve for XAML bindings for such properties. - /// - /// - /// Property should have the tree ancestor of the ItemsControl type or - /// marked with the attribute. - /// - [AttributeUsage(AttributeTargets.Property)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class XamlItemBindingOfItemsControlAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspChildControlTypeAttribute : Attribute - { - public AspChildControlTypeAttribute(string tagName, Type controlType) - { - TagName = tagName; - ControlType = controlType; - } - - public string TagName { get; private set; } - public Type ControlType { get; private set; } - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspDataFieldAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspDataFieldsAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Property)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspMethodPropertyAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspRequiredAttributeAttribute : Attribute - { - public AspRequiredAttributeAttribute([NotNull] string attribute) - { - Attribute = attribute; - } - - public string Attribute { get; private set; } - } - - [AttributeUsage(AttributeTargets.Property)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class AspTypePropertyAttribute : Attribute - { - public bool CreateConstructorReferences { get; private set; } - - public AspTypePropertyAttribute(bool createConstructorReferences) - { - CreateConstructorReferences = createConstructorReferences; - } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RazorImportNamespaceAttribute : Attribute - { - public RazorImportNamespaceAttribute(string name) - { - Name = name; - } - - public string Name { get; private set; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RazorInjectionAttribute : Attribute - { - public RazorInjectionAttribute(string type, string fieldName) - { - Type = type; - FieldName = fieldName; - } - - public string Type { get; private set; } - public string FieldName { get; private set; } - } - - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RazorHelperCommonAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Property)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RazorLayoutAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RazorWriteLiteralMethodAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Method)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RazorWriteMethodAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Parameter)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class RazorWriteMethodParameterAttribute : Attribute - { - } - - /// - /// Prevents the Member Reordering feature from tossing members of the marked class. - /// - /// - /// The attribute must be mentioned in your member reordering patterns. - /// - [AttributeUsage(AttributeTargets.All)] - [Conditional("JETBRAINS_ANNOTATIONS")] - public sealed class NoReorder : Attribute - { - } -} diff --git a/src/analyzers.ruleset b/src/analyzers.ruleset index 23cb2aa88..cbedd0b51 100644 --- a/src/analyzers.ruleset +++ b/src/analyzers.ruleset @@ -10,17 +10,16 @@ + - - @@ -32,7 +31,6 @@ - @@ -65,7 +63,7 @@ - + @@ -84,6 +82,7 @@ + diff --git a/src/stylecop.json b/src/stylecop.json new file mode 100644 index 000000000..a791c15b6 --- /dev/null +++ b/src/stylecop.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "indentation": { + "useTabs": false, + "indentationSize": 4 + }, + "documentationRules": { + "documentExposedElements": true, + "documentInternalElements": false, + "documentPrivateElements": false, + "documentInterfaces": true, + "documentPrivateFields": false, + "documentationCulture": "en-US", + "companyName": "Roland Pheasant", + "copyrightText": "Copyright (c) 2011-2020 {companyName}. All rights reserved.\n{companyName} licenses this file to you under the {licenseName} license.\nSee the LICENSE file in the project root for full license information.", + "variables": { + "licenseName": "MIT", + "licenseFile": "LICENSE" + }, + "xmlHeader": false + }, + "layoutRules": { + "newlineAtEndOfFile": "allow", + "allowConsecutiveUsings": true + }, + "maintainabilityRules": { + "topLevelTypes": [ + "class", + "interface", + "struct", + "enum", + "delegate" + ] + }, + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace", + "systemUsingDirectivesFirst": true + }, + "namingRules": { + "tupleElementNameCasing": "camelCase" + } + } +}