├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature_request.yml ├── renovate.json └── workflows │ ├── ci-build.yml │ ├── lock.yml │ ├── pr-test-coverage.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── NonProduction └── DynamicData.Profile │ ├── Allocations.cs │ ├── AllocationsCount.cs │ ├── CacheAllocationChecks.cs │ ├── CheapAndDirtyAllocationTest.cs │ ├── DynamicData.Profile.csproj │ └── Person.cs ├── README.md ├── ReleaseNotes.md ├── images ├── dd-logo.svg ├── dd-shapes.svg └── logo.png ├── nuget.config ├── src ├── Directory.Build.props ├── Directory.build.targets ├── DynamicData.Benchmarks │ ├── .editorconfig │ ├── Cache │ │ ├── DisposeMany_Cache.cs │ │ ├── EditDiff.cs │ │ ├── ExpireAfter_Cache_ForSource.cs │ │ ├── ExpireAfter_Cache_ForStream.cs │ │ ├── FilterImmutable.cs │ │ ├── Filter_Cache_WithPredicateState.cs │ │ ├── SortAndBindChange.cs │ │ ├── SortAndBindInitial.cs │ │ ├── SourceCache.cs │ │ ├── StatelessFiltering.cs │ │ ├── StatelessTransforming.cs │ │ ├── ToObservableChangeSet_Cache.cs │ │ ├── TransformImmutable.cs │ │ └── TransformMany.cs │ ├── DynamicData.Benchmarks.csproj │ ├── List │ │ ├── DisposeMany_List.cs │ │ ├── ExpireAfter_List.cs │ │ ├── Filter_List_WithPredicateState.cs │ │ ├── GroupAdd.cs │ │ ├── GroupRemove.cs │ │ ├── SourceList.cs │ │ └── ToObservableChangeSet_List.cs │ ├── Polyfills │ │ ├── CompilerFeatureRequiredAttribute.cs │ │ ├── IsExternalInit.cs │ │ └── RequiredMemberAttribute.cs │ └── Program.cs ├── DynamicData.Tests │ ├── API │ │ ├── ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt │ │ ├── ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt │ │ ├── ApiApprovalTests.cs │ │ └── ApiExtensions.cs │ ├── AggregationTests │ │ ├── AggregationFixture.cs │ │ ├── AverageFixture.cs │ │ ├── MaxFixture.cs │ │ ├── MinFixture.cs │ │ └── SumFixture.cs │ ├── AutoRefreshFilter.cs │ ├── Binding │ │ ├── AvaloniaDictionaryFixture.cs │ │ ├── BindingLIstBindListFixture.cs │ │ ├── BindingListBindCacheFixture.cs │ │ ├── BindingListBindCacheSortedFixture.cs │ │ ├── BindingListToChangeSetFixture.cs │ │ ├── DeeplyNestedNotifyPropertyChangedFixture.cs │ │ ├── IObservableListBindCacheFixture.cs │ │ ├── IObservableListBindCacheSortedFixture.cs │ │ ├── IObservableListBindListFixture.cs │ │ ├── NotifyPropertyChangedExFixture.cs │ │ ├── ObservableCollectionBindCacheFixture.cs │ │ ├── ObservableCollectionBindCacheSortedFixture.cs │ │ ├── ObservableCollectionBindListFixture.cs │ │ ├── ObservableCollectionExtendedToChangeSetFixture.cs │ │ ├── ObservableCollectionToChangeSetFixture.cs │ │ ├── ReadOnlyObservableCollectionToChangeSetFixture.cs │ │ └── ReadonlyCollectionBindCacheFixture.cs │ ├── Cache │ │ ├── AndFixture.cs │ │ ├── AsyncDisposeManyFixture.cs │ │ ├── AutoRefreshFixture.cs │ │ ├── BatchFixture.cs │ │ ├── BatchIfFixture.cs │ │ ├── BatchIfWithTimeOutFixture.cs │ │ ├── BufferInitialFixture.cs │ │ ├── DeferUntilLoadedFixture.cs │ │ ├── DisposeManyFixture.cs │ │ ├── DistinctFixture.cs │ │ ├── DynamicAndFixture.cs │ │ ├── DynamicExceptFixture.cs │ │ ├── DynamicOrFixture.cs │ │ ├── DynamicXorFixture.cs │ │ ├── EditDiffChangeSetFixture.cs │ │ ├── EditDiffChangeSetOptionalFixture.cs │ │ ├── EditDiffFixture.cs │ │ ├── EnsureUniqueKeysFixture.cs │ │ ├── EnumerableObservableToObservableChangeSetFixture.cs │ │ ├── ExceptFixture.cs │ │ ├── ExpireAfterFixture.ForSource.cs │ │ ├── ExpireAfterFixture.ForStream.cs │ │ ├── ExpireAfterFixture.cs │ │ ├── FilterControllerFixture.cs │ │ ├── FilterFixture.WithPredicateState.cs │ │ ├── FilterFixture.cs │ │ ├── FilterImmutableFixture.cs │ │ ├── FilterOnConnectFixture.cs │ │ ├── FilterOnObservableFixture.cs │ │ ├── FilterOnPropertyFixture.cs │ │ ├── FilterParallelFixture.cs │ │ ├── ForEachChangeFixture.cs │ │ ├── FromAsyncFixture.cs │ │ ├── FullJoinFixture.cs │ │ ├── FullJoinManyFixture.cs │ │ ├── GroupControllerFixture.cs │ │ ├── GroupControllerForFilteredItemsFixture.cs │ │ ├── GroupFixture.cs │ │ ├── GroupFromDistinctFixture.cs │ │ ├── GroupImmutableFixture.cs │ │ ├── GroupOnDynamicFixture.cs │ │ ├── GroupOnObservableFixture.cs │ │ ├── GroupOnPropertyFixture.cs │ │ ├── GroupOnPropertyWithImmutableStateFixture.cs │ │ ├── IgnoreUpdateFixture.cs │ │ ├── IncludeUpdateFixture.cs │ │ ├── InnerJoinFixture.cs │ │ ├── InnerJoinFixtureRaceCondition.cs │ │ ├── InnerJoinManyFixture.cs │ │ ├── KeyValueCollectionEx.cs │ │ ├── LeftJoinFixture.cs │ │ ├── LeftJoinManyFixture.cs │ │ ├── MergeChangeSetsFixture.cs │ │ ├── MergeManyChangeSetsCacheFixture.cs │ │ ├── MergeManyChangeSetsCacheSourceCompareFixture.cs │ │ ├── MergeManyChangeSetsListFixture.cs │ │ ├── MergeManyFixture.cs │ │ ├── MergeManyItemsFixture.cs │ │ ├── MergeManyWithKeyOverloadFixture.cs │ │ ├── MonitorStatusFixture.cs │ │ ├── ObservableCachePreviewFixture.cs │ │ ├── ObservableChangeSetFixture.cs │ │ ├── ObservableToObservableChangeSetFixture.cs │ │ ├── OfTypeFixture.cs │ │ ├── OnItemFixture.cs │ │ ├── OrFixture.cs │ │ ├── PageFixture.cs │ │ ├── QueryWhenChangedFixture.cs │ │ ├── RefCountFixture.cs │ │ ├── RightJoinFixture.cs │ │ ├── RightJoinManyFixture.cs │ │ ├── SizeLimitFixture.cs │ │ ├── SortAndBindFixture.cs │ │ ├── SortAndBindObservableFixture.cs │ │ ├── SortAndPageAndBindFixture.cs │ │ ├── SortAndPageFixture.cs │ │ ├── SortAndVirtualizeAndBindFixture.cs │ │ ├── SortAndVirtualizeFixture.cs │ │ ├── SortFixture.cs │ │ ├── SortObservableFixtureFixture.cs │ │ ├── SourceCacheFixture.cs │ │ ├── SubscribeManyFixture.cs │ │ ├── SuspendNotificationsFixture.cs │ │ ├── SwitchFixture.cs │ │ ├── ToObservableChangeSetFixture.cs │ │ ├── ToObservableOptionalFixture.cs │ │ ├── ToSortedCollectionFixture.cs │ │ ├── TransformAsyncFixture.cs │ │ ├── TransformFixture.cs │ │ ├── TransformFixtureParallel.cs │ │ ├── TransformImmutableFixture.cs │ │ ├── TransformManyAsyncFixture.cs │ │ ├── TransformManyFixture.cs │ │ ├── TransformManyObservableCacheFixture.cs │ │ ├── TransformManyRefreshFixture.cs │ │ ├── TransformManySimpleFixture.cs │ │ ├── TransformOnObservableFixture.cs │ │ ├── TransformSafeAsyncFixture.cs │ │ ├── TransformSafeFixture.cs │ │ ├── TransformSafeParallelFixture.cs │ │ ├── TransformTreeFixture.cs │ │ ├── TransformTreeWithRefreshFixture.cs │ │ ├── TransformWithInlineUpdateFixture.cs │ │ ├── TrueForAllFixture.cs │ │ ├── TrueForAnyFixture.cs │ │ ├── WatchFixture.cs │ │ ├── WatcherFixture.cs │ │ └── XorFixture.cs │ ├── Domain │ │ ├── Animal.cs │ │ ├── AnimalOwner.cs │ │ ├── Fakers.cs │ │ ├── Market.cs │ │ ├── MarketPrice.cs │ │ ├── ParentAndChildren.cs │ │ ├── ParentChild.cs │ │ ├── Person.cs │ │ ├── PersonEmployment.cs │ │ ├── PersonObs.cs │ │ ├── PersonWithChildren.cs │ │ ├── PersonWithEmployment.cs │ │ ├── PersonWithFriends.cs │ │ ├── PersonWithGender.cs │ │ ├── PersonWithRelations.cs │ │ ├── Pet.cs │ │ ├── RandomPersonGenerator.cs │ │ └── SelfObservingPerson.cs │ ├── DynamicData.Tests.csproj │ ├── EnumerableExFixtures.cs │ ├── EnumerableIListFixture.cs │ ├── Issues │ │ ├── EmptyToChangeSetIssue.cs │ │ ├── OnItemRemovedIssue.cs │ │ └── Readme.txt │ ├── Kernal │ │ ├── CacheUpdaterFixture.cs │ │ ├── DistinctUpdateFixture.cs │ │ ├── EnumerableEx.cs │ │ ├── KeyValueFixture.cs │ │ ├── OptionFixture.cs │ │ ├── OptionObservableFixture.cs │ │ ├── SourceUpdaterFixture.cs │ │ └── UpdateFixture.cs │ ├── List │ │ ├── AndFixture.cs │ │ ├── AutoRefreshFixture.cs │ │ ├── BatchFixture.cs │ │ ├── BatchIfFixture.cs │ │ ├── BatchIfWithTimeOutFixture.cs │ │ ├── BufferFixture.cs │ │ ├── BufferInitialFixture.cs │ │ ├── CastFixture.cs │ │ ├── ChangeAwareListFixture.cs │ │ ├── ChangeSetFixture.cs │ │ ├── CloneChangesFixture.cs │ │ ├── CloneFixture.cs │ │ ├── CreationFixtures.cs │ │ ├── DeferUntilLoadedFixture.cs │ │ ├── DisposeManyFixture.cs │ │ ├── DistinctValuesFixture.cs │ │ ├── DynamicAndFixture.cs │ │ ├── DynamicExceptFixture.cs │ │ ├── DynamicOrFixture.cs │ │ ├── DynamicXOrFixture.cs │ │ ├── EditDiffFixture.cs │ │ ├── ExceptFixture.cs │ │ ├── ExpireAfterFixture.cs │ │ ├── FilterControllerFixtureWithClearAndReplace.cs │ │ ├── FilterControllerFixtureWithDiffSet.cs │ │ ├── FilterFixture.WithPredicateState.cs │ │ ├── FilterFixture.cs │ │ ├── FilterOnObservableFixture.cs │ │ ├── FilterOnPropertyFixture.cs │ │ ├── FilterWithObservable.cs │ │ ├── ForEachChangeFixture.cs │ │ ├── FromAsyncFixture.cs │ │ ├── GroupImmutableFixture.cs │ │ ├── GroupOnFixture.cs │ │ ├── GroupOnPropertyFixture.cs │ │ ├── GroupOnPropertyWithImmutableStateFixture.cs │ │ ├── MergeChangeSetsFixture.cs │ │ ├── MergeManyChangeSetsCacheFixture.cs │ │ ├── MergeManyChangeSetsFixture.cs │ │ ├── MergeManyChangeSetsListFixture.cs │ │ ├── MergeManyFixture.cs │ │ ├── OnItemFixture.cs │ │ ├── OrFixture.cs │ │ ├── PageFixture.cs │ │ ├── QueryWhenChangedFixture.cs │ │ ├── RecursiveTransformManyFixture.cs │ │ ├── RefCountFixture.cs │ │ ├── RemoveManyFixture.cs │ │ ├── ReverseFixture.cs │ │ ├── SelectFixture.cs │ │ ├── SizeLimitFixture.cs │ │ ├── SortFixture.cs │ │ ├── SortMutableFixture.cs │ │ ├── SortPrimitiveFixture.cs │ │ ├── SourceListFixture.cs │ │ ├── SourceListPreviewFixture.cs │ │ ├── SubscribeManyFixture.cs │ │ ├── SwitchFixture.cs │ │ ├── ToCollectionFixture.cs │ │ ├── ToObservableChangeSetFixture.cs │ │ ├── TransformAsyncFixture.cs │ │ ├── TransformFixture.cs │ │ ├── TransformManyFixture.cs │ │ ├── TransformManyObservableCollectionFixture.cs │ │ ├── TransformManyProjectionFixture.cs │ │ ├── TransformManyRefreshFixture.cs │ │ ├── VirtualisationFixture.cs │ │ └── XOrFixture.cs │ ├── ObservableCollectionExFixture.cs │ └── Utilities │ │ ├── CacheItemRecordingObserver.cs │ │ ├── ComparerExtensions.cs │ │ ├── FakeScheduler.cs │ │ ├── FakerExtensions.cs │ │ ├── FunctionalExtensions.cs │ │ ├── ListItemRecordingObserver.cs │ │ ├── ObservableEx.cs │ │ ├── ObservableExtensions.cs │ │ ├── ObservableSpy.cs │ │ ├── RandomizerExtensions.cs │ │ ├── RawAnonymousObservable.cs │ │ ├── RawAnonymousObserver.cs │ │ ├── RecordingObserverBase.cs │ │ ├── SelectManyExtensions.cs │ │ ├── StressAddRemoveExtensions.cs │ │ ├── TestSourceCache.cs │ │ ├── TestSourceList.cs │ │ ├── UnsynchronizedNotificationException.cs │ │ └── ValueRecordingObserver.cs ├── DynamicData.sln ├── DynamicData │ ├── Aggregation │ │ ├── AggregateEnumerator.cs │ │ ├── AggregateItem.cs │ │ ├── AggregateType.cs │ │ ├── AggregationEx.cs │ │ ├── Avg.cs │ │ ├── AvgEx.cs │ │ ├── CountEx.cs │ │ ├── IAggregateChangeSet.cs │ │ ├── MaxEx.cs │ │ ├── StdDev.cs │ │ ├── StdDevEx.cs │ │ └── SumEx.cs │ ├── Alias │ │ ├── ObservableCacheAlias.cs │ │ └── ObservableListAlias.cs │ ├── Attributes.cs │ ├── Binding │ │ ├── AbstractNotifyPropertyChanged.cs │ │ ├── BindPaged.cs │ │ ├── BindVirtualized.cs │ │ ├── BindingListAdaptor.cs │ │ ├── BindingListEventsSuspender.cs │ │ ├── BindingListEx.cs │ │ ├── BindingOptions.cs │ │ ├── ExpressionBuilder.cs │ │ ├── IEvaluateAware.cs │ │ ├── IIndexAware.cs │ │ ├── INotifyCollectionChangedSuspender.cs │ │ ├── IObservableCollection.cs │ │ ├── IObservableCollectionAdaptor.cs │ │ ├── IObservableListEx.cs │ │ ├── ISortedObservableCollectionAdaptor.cs │ │ ├── NotifyPropertyChangedEx.cs │ │ ├── Observable.cs │ │ ├── ObservableCollectionAdaptor.cs │ │ ├── ObservableCollectionEx.cs │ │ ├── ObservableCollectionExtended.cs │ │ ├── ObservablePropertyFactory.cs │ │ ├── ObservablePropertyFactoryCache.cs │ │ ├── ObservablePropertyPart.cs │ │ ├── PropertyValue.cs │ │ ├── SortAndBind.cs │ │ ├── SortAndBindOptions.cs │ │ ├── SortDirection.cs │ │ ├── SortExpression.cs │ │ ├── SortExpressionComparer.cs │ │ ├── SortedBindingListAdaptor.cs │ │ └── SortedObservableCollectionAdaptor.cs │ ├── Cache │ │ ├── CacheChangeSetEx.cs │ │ ├── Change.cs │ │ ├── ChangeAwareCache.cs │ │ ├── ChangeReason.cs │ │ ├── ChangeSet.cs │ │ ├── DistinctChangeSet.cs │ │ ├── GroupChangeSet.cs │ │ ├── ICache.cs │ │ ├── ICacheUpdater.cs │ │ ├── IChangeSet.cs │ │ ├── IChangeSetAdaptor.cs │ │ ├── IConnectableCache.cs │ │ ├── IDistinctChangeSet.cs │ │ ├── IGroup.cs │ │ ├── IGroupChangeSet.cs │ │ ├── IGrouping.cs │ │ ├── IImmutableGroupChangeSet.cs │ │ ├── IIntermediateCache.cs │ │ ├── IKey.cs │ │ ├── IKeyValue.cs │ │ ├── IKeyValueCollection.cs │ │ ├── IObservableCache.cs │ │ ├── IPageRequest.cs │ │ ├── IPageResponse.cs │ │ ├── IPagedChangeSet.cs │ │ ├── IQuery.cs │ │ ├── ISortedChangeSet.cs │ │ ├── ISortedChangeSetAdaptor.cs │ │ ├── ISourceCache.cs │ │ ├── ISourceUpdater.cs │ │ ├── IVirtualChangeSet.cs │ │ ├── IVirtualRequest.cs │ │ ├── IVirtualResponse.cs │ │ ├── IndexedItem.cs │ │ ├── IntermediateCache.cs │ │ ├── Internal │ │ │ ├── AbstractFilter.cs │ │ │ ├── AnonymousObservableCache.cs │ │ │ ├── AnonymousQuery.cs │ │ │ ├── AsyncDisposeMany.cs │ │ │ ├── AutoRefresh.cs │ │ │ ├── BatchIf.cs │ │ │ ├── Cache.cs │ │ │ ├── CacheEx.cs │ │ │ ├── CacheUpdater.cs │ │ │ ├── Cast.cs │ │ │ ├── ChangeSetCache.cs │ │ │ ├── ChangeSetMergeTracker.cs │ │ │ ├── CombineOperator.cs │ │ │ ├── Combiner.cs │ │ │ ├── DeferUntilLoaded.cs │ │ │ ├── DictionaryExtensions.cs │ │ │ ├── DisposeMany.cs │ │ │ ├── DistinctCalculator.cs │ │ │ ├── DynamicCombiner.cs │ │ │ ├── DynamicFilter.cs │ │ │ ├── DynamicGrouper.cs │ │ │ ├── EditDiff.cs │ │ │ ├── EditDiffChangeSet.cs │ │ │ ├── EditDiffChangeSetOptional.cs │ │ │ ├── ExpirableItem.cs │ │ │ ├── ExpireAfter.ForSource.cs │ │ │ ├── ExpireAfter.ForStream.cs │ │ │ ├── Filter.WithPredicateState.cs │ │ │ ├── FilterEx.cs │ │ │ ├── FilterImmutable.cs │ │ │ ├── FilterOnObservable.cs │ │ │ ├── FilterOnProperty.cs │ │ │ ├── FilteredIndexCalculator.cs │ │ │ ├── FinallySafe.cs │ │ │ ├── FullJoin.cs │ │ │ ├── FullJoinMany.cs │ │ │ ├── GroupOn.cs │ │ │ ├── GroupOnDynamic.cs │ │ │ ├── GroupOnImmutable.cs │ │ │ ├── GroupOnObservable.cs │ │ │ ├── GroupOnProperty.cs │ │ │ ├── GroupOnPropertyWithImmutableState.cs │ │ │ ├── IFilter.cs │ │ │ ├── IKeySelector.cs │ │ │ ├── ImmutableGroup.cs │ │ │ ├── ImmutableGroupChangeSet.cs │ │ │ ├── IndexAndNode.cs │ │ │ ├── IndexCalculator.cs │ │ │ ├── InnerJoin.cs │ │ │ ├── InnerJoinMany.cs │ │ │ ├── KeyComparer.cs │ │ │ ├── KeySelector.cs │ │ │ ├── KeySelectorException.cs │ │ │ ├── KeyValueCollection.cs │ │ │ ├── KeyValueComparer.cs │ │ │ ├── LeftJoin.cs │ │ │ ├── LeftJoinMany.cs │ │ │ ├── LockFreeObservableCache.cs │ │ │ ├── ManagedGroup.cs │ │ │ ├── MergeChangeSets.cs │ │ │ ├── MergeMany.cs │ │ │ ├── MergeManyCacheChangeSets.cs │ │ │ ├── MergeManyCacheChangeSetsSourceCompare.cs │ │ │ ├── MergeManyItems.cs │ │ │ ├── MergeManyListChangeSets.cs │ │ │ ├── ObservableWithValue.cs │ │ │ ├── OfType.cs │ │ │ ├── OnBeingRemoved.cs │ │ │ ├── Page.cs │ │ │ ├── QueryWhenChanged.cs │ │ │ ├── ReaderWriter.cs │ │ │ ├── RefCount.cs │ │ │ ├── RemoveKeyEnumerator.cs │ │ │ ├── RightJoin.cs │ │ │ ├── RightJoinMany.cs │ │ │ ├── SizeExpirer.cs │ │ │ ├── SizeLimiter.cs │ │ │ ├── Sort.cs │ │ │ ├── SortAndPage.cs │ │ │ ├── SortAndVirtualize.cs │ │ │ ├── SortExtensions.cs │ │ │ ├── SortedKeyValueApplicator.cs │ │ │ ├── SpecifiedGrouper.cs │ │ │ ├── StaticFilter.cs │ │ │ ├── StatusMonitor.cs │ │ │ ├── SubscribeMany.cs │ │ │ ├── Switch.cs │ │ │ ├── ToObservableChangeSet.cs │ │ │ ├── ToObservableOptional.cs │ │ │ ├── Transform.cs │ │ │ ├── TransformAsync.cs │ │ │ ├── TransformImmutable.cs │ │ │ ├── TransformMany.cs │ │ │ ├── TransformManyAsync.cs │ │ │ ├── TransformOnObservable.cs │ │ │ ├── TransformWithForcedTransform.cs │ │ │ ├── TransformWithInlineUpdate.cs │ │ │ ├── TreeBuilder.cs │ │ │ ├── TrueFor.cs │ │ │ ├── UniquenessEnforcer.cs │ │ │ └── Virtualise.cs │ │ ├── MissingKeyException.cs │ │ ├── Node.cs │ │ ├── ObservableCache.cs │ │ ├── ObservableCacheEx.SortAndBind.cs │ │ ├── ObservableCacheEx.VirtualiseAndPage.cs │ │ ├── ObservableCacheEx.cs │ │ ├── PageContext.cs │ │ ├── PageRequest.cs │ │ ├── PageResponse.cs │ │ ├── PagedChangeSet.cs │ │ ├── SortAndPageOptions.cs │ │ ├── SortAndVirtualizeOptions.cs │ │ ├── SortOptimisations.cs │ │ ├── SortReason.cs │ │ ├── SortedChangeSet.cs │ │ ├── SourceCache.cs │ │ ├── SourceCacheEx.cs │ │ ├── Tests │ │ │ ├── ChangeSetAggregator.cs │ │ │ ├── DistinctChangeSetAggregator.cs │ │ │ ├── GroupChangeSetAggregator.cs │ │ │ ├── PagedChangeSetAggregator.cs │ │ │ ├── SortedChangeSetAggregator.cs │ │ │ ├── TestEx.cs │ │ │ └── VirtualChangeSetAggregator.cs │ │ ├── TransformAsyncOptions.cs │ │ ├── VirtualChangeSet.cs │ │ ├── VirtualContext.cs │ │ ├── VirtualRequest.cs │ │ └── VirtualResponse.cs │ ├── Constants.cs │ ├── Diagnostics │ │ ├── ChangeStatistics.cs │ │ ├── ChangeSummary.cs │ │ └── DiagnosticOperators.cs │ ├── DynamicData.csproj │ ├── DynamicData.csproj.DotSettings │ ├── DynamicDataOptions.cs │ ├── EnumerableEx.cs │ ├── Experimental │ │ ├── ExperimentalEx.cs │ │ ├── ISubjectWithRefCount.cs │ │ ├── IWatcher.cs │ │ ├── SubjectWithRefCount.cs │ │ └── Watcher.cs │ ├── GlobalConfig.cs │ ├── IChangeSet.cs │ ├── Internal │ │ ├── CacheParentSubscription.cs │ │ ├── ExceptionMixins.cs │ │ ├── KeyedDisposable.cs │ │ ├── ObservableEx.cs │ │ ├── Rxx.cs │ │ └── SwappableLock.cs │ ├── Kernel │ │ ├── ConnectionStatus.cs │ │ ├── EnumerableEx.cs │ │ ├── EnumerableIList.cs │ │ ├── EnumeratorIList.cs │ │ ├── Error.cs │ │ ├── IEnumerableIList.cs │ │ ├── ISupportsCapacity.cs │ │ ├── InternalEx.cs │ │ ├── ItemWithIndex.cs │ │ ├── ItemWithValue.cs │ │ ├── OptionElse.cs │ │ ├── OptionExtensions.cs │ │ ├── OptionObservableExtensions.cs │ │ ├── Optional.cs │ │ ├── ParallelEx.cs │ │ ├── ReadOnlyCollectionLight.cs │ │ └── ReferenceEqualityComparer.cs │ ├── List │ │ ├── Change.cs │ │ ├── ChangeAwareList.cs │ │ ├── ChangeAwareListWithRefCounts.cs │ │ ├── ChangeSet.cs │ │ ├── ChangeSetEx.cs │ │ ├── ChangeType.cs │ │ ├── IChangeSet.cs │ │ ├── IChangeSetAdaptor.cs │ │ ├── IExtendedList.cs │ │ ├── IGroup.cs │ │ ├── IGrouping.cs │ │ ├── IObservableList.cs │ │ ├── IPageChangeSet.cs │ │ ├── ISourceList.cs │ │ ├── IVirtualChangeSet.cs │ │ ├── Internal │ │ │ ├── AnonymousObservableList.cs │ │ │ ├── AutoRefresh.cs │ │ │ ├── BufferIf.cs │ │ │ ├── ChangeSetMergeTracker.cs │ │ │ ├── ClonedListChangeSet.cs │ │ │ ├── Combiner.cs │ │ │ ├── DeferUntilLoaded.cs │ │ │ ├── DisposeMany.cs │ │ │ ├── Distinct.cs │ │ │ ├── DynamicCombiner.cs │ │ │ ├── EditDiff.cs │ │ │ ├── ExpirableItem.cs │ │ │ ├── ExpireAfter.cs │ │ │ ├── Filter.WithPredicateState.cs │ │ │ ├── Filter.cs │ │ │ ├── FilterOnObservable.cs │ │ │ ├── FilterOnProperty.cs │ │ │ ├── FilterStatic.cs │ │ │ ├── Group.cs │ │ │ ├── GroupOn.cs │ │ │ ├── GroupOnImmutable.cs │ │ │ ├── GroupOnProperty.cs │ │ │ ├── GroupOnPropertyWithImmutableState.cs │ │ │ ├── ImmutableGroup.cs │ │ │ ├── LimitSizeTo.cs │ │ │ ├── MergeChangeSets.cs │ │ │ ├── MergeMany.cs │ │ │ ├── MergeManyCacheChangeSets.cs │ │ │ ├── MergeManyListChangeSets.cs │ │ │ ├── OnBeingAdded.cs │ │ │ ├── OnBeingRemoved.cs │ │ │ ├── Pager.cs │ │ │ ├── QueryWhenChanged.cs │ │ │ ├── ReaderWriter.cs │ │ │ ├── RefCount.cs │ │ │ ├── ReferenceCountTracker.cs │ │ │ ├── Sort.cs │ │ │ ├── SubscribeMany.cs │ │ │ ├── Switch.cs │ │ │ ├── ToObservableChangeSet.cs │ │ │ ├── TransformAsync.cs │ │ │ ├── TransformMany.cs │ │ │ ├── Transformer.cs │ │ │ ├── UnifiedChange.cs │ │ │ └── Virtualiser.cs │ │ ├── ItemChange.cs │ │ ├── Linq │ │ │ ├── AddKeyEnumerator.cs │ │ │ ├── ItemChangeEnumerator.cs │ │ │ ├── Reverser.cs │ │ │ ├── UnifiedChangeEnumerator.cs │ │ │ └── WithoutIndexEnumerator.cs │ │ ├── ListChangeReason.cs │ │ ├── ListEx.cs │ │ ├── ListFilterPolicy.cs │ │ ├── ObservableListEx.cs │ │ ├── PageChangeSet.cs │ │ ├── RangeChange.cs │ │ ├── SortException.cs │ │ ├── SortOptions.cs │ │ ├── SourceList.cs │ │ ├── SourceListEditConvenienceEx.cs │ │ ├── SourceListEx.cs │ │ ├── Tests │ │ │ ├── ChangeSetAggregator.cs │ │ │ └── ListTextEx.cs │ │ ├── UnspecifiedIndexException.cs │ │ └── VirtualChangeSet.cs │ ├── ObservableChangeSet.cs │ ├── ObsoleteEx.cs │ ├── Platforms │ │ └── net45 │ │ │ ├── PFilter.cs │ │ │ ├── PSubscribeMany.cs │ │ │ ├── PTransform.cs │ │ │ ├── ParallelEx.cs │ │ │ ├── ParallelOperators.cs │ │ │ ├── ParallelType.cs │ │ │ └── ParallelisationOptions.cs │ └── Polyfills │ │ ├── CompilerFeatureRequiredAttribute.cs │ │ ├── DynamicallyAccessedMembersAttribute.cs │ │ ├── EnumEx.cs │ │ ├── IsExternalInit.cs │ │ ├── ListEnsureCapacity.cs │ │ └── RequiredMemberAttribute.cs ├── coverlet.xml ├── global.json └── stylecop.json └── version.json /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions 4 | url: https://github.com/reactivemarbles/DynamicData/discussions 5 | about: 'For general questions about DynamicData, ask in the GitHub discussions' 6 | - name: Chat 7 | url: https://www.reactiveui.net/slack 8 | about: 'Our slack chat community invite' 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Create a request for functionality enhancement 3 | title: "[Feature]: " 4 | labels: ["feature"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ## Please note although we can't commit to any timeline 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Describe the functionality desired 🐞 14 | description: A clear and concise description of what the feature request is. 15 | value: "A feature request" 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: functionality-steps 20 | attributes: 21 | label: The steps the functionality will provide 22 | description: "Steps of the desired functionality:" 23 | value: | 24 | 1. Go to '...' 25 | 2. Click on '....' 26 | 3. Scroll down to '....' 27 | 4. See error 28 | validations: 29 | required: true 30 | - type: input 31 | id: considerations 32 | attributes: 33 | label: Considerations 34 | description: "What attempts have you attempted with the existing functionality?" 35 | validations: 36 | required: true 37 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>reactivemarbles/.github:renovate" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | env: 8 | configuration: Release 9 | productNamespacePrefix: "DynamicData" 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-2022 14 | outputs: 15 | nbgv: ${{ steps.nbgv.outputs.SemVer2 }} 16 | steps: 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | lfs: true 23 | 24 | - name: Setup .NET 6/7/8/9 25 | uses: actions/setup-dotnet@v4 26 | with: 27 | dotnet-version: | 28 | 6.0.x 29 | 7.0.x 30 | 8.0.x 31 | 9.0.x 32 | 33 | - name: NBGV 34 | id: nbgv 35 | uses: dotnet/nbgv@master 36 | with: 37 | setAllVars: true 38 | 39 | - name: NuGet Restore 40 | run: dotnet restore DynamicData.sln 41 | working-directory: src 42 | 43 | - name: Build 44 | run: dotnet build --no-restore --configuration Release DynamicData.sln 45 | working-directory: src 46 | 47 | - name: Run Tests 48 | run: dotnet test --no-restore --configuration Release DynamicData.sln 49 | working-directory: src 50 | 51 | - name: Pack 52 | run: dotnet pack --no-restore --configuration Release DynamicData.sln 53 | working-directory: src 54 | 55 | - name: Create NuGet Artifacts 56 | uses: actions/upload-artifact@master 57 | with: 58 | name: nuget 59 | path: '**/*.nupkg' 60 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v5 20 | with: 21 | github-token: ${{ github.token }} 22 | issue-inactive-days: '14' 23 | pr-inactive-days: '14' 24 | issue-comment: > 25 | This issue has been automatically locked since there 26 | has not been any recent activity after it was closed. 27 | Please open a new issue for related bugs. 28 | pr-comment: > 29 | This pull request has been automatically locked since there 30 | has not been any recent activity after it was closed. 31 | Please open a new issue for related bugs. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Roland Pheasant 2011-2022 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /NonProduction/DynamicData.Profile/Allocations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | namespace DynamicData.Profile 8 | { 9 | public static class Allocations 10 | { 11 | public static AllocationsCount Run(Action action) 12 | { 13 | var initial = GC.GetAllocatedBytesForCurrentThread(); 14 | action(); 15 | var final = GC.GetAllocatedBytesForCurrentThread(); 16 | 17 | return new AllocationsCount(initial, final); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /NonProduction/DynamicData.Profile/AllocationsCount.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Profile 6 | { 7 | public class AllocationsCount 8 | { 9 | 10 | public long InitialSize { get; } 11 | public long FinalSize { get; } 12 | public long Size => FinalSize - InitialSize; 13 | 14 | /// Initializes a new instance of the class. 15 | public AllocationsCount(long initialSize, long finalSize) 16 | { 17 | InitialSize = initialSize; 18 | FinalSize = finalSize; 19 | } 20 | 21 | public override string ToString() 22 | { 23 | return $"Allocation Bytes: {Size:N0}"; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /NonProduction/DynamicData.Profile/DynamicData.Profile.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | $(NoWarn);CS0618 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /NonProduction/DynamicData.Profile/Person.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Profile 6 | { 7 | public class Person 8 | { 9 | public string Name { get; } 10 | public int Age { get; } 11 | 12 | public Person(string name, int age) 13 | { 14 | Name = name; 15 | Age = age; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactivemarbles/DynamicData/5f9a27c7e9830de4d0d325be1a5f228a48693fd5/images/logo.png -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/Cache/SourceCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Linq; 6 | 7 | using BenchmarkDotNet.Attributes; 8 | using BenchmarkDotNet.Jobs; 9 | 10 | namespace DynamicData.Benchmarks.Cache 11 | { 12 | public class BenchmarkItem 13 | { 14 | public int Id { get; } 15 | 16 | public BenchmarkItem(int id) 17 | { 18 | Id = id; 19 | } 20 | } 21 | 22 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 23 | [MemoryDiagnoser] 24 | [MarkdownExporterAttribute.GitHub] 25 | public class SourceCache 26 | { 27 | private SourceCache? _cache; 28 | private BenchmarkItem[] _items = Enumerable.Range(1, 100).Select(i => new BenchmarkItem(i)).ToArray(); 29 | 30 | [GlobalSetup] 31 | public void Setup() 32 | { 33 | _cache = new SourceCache(i => i.Id); 34 | } 35 | 36 | [Params(1, 100, 1_000, 10_000, 100_000)] 37 | public int N; 38 | 39 | [IterationSetup] 40 | public void SetupIteration() 41 | { 42 | _cache?.Clear(); 43 | _items = Enumerable.Range(1, N).Select(i => new BenchmarkItem(i)).ToArray(); 44 | } 45 | 46 | [GlobalCleanup] 47 | public void Teardown() 48 | { 49 | _cache?.Dispose(); 50 | _cache = null; 51 | } 52 | 53 | [Benchmark] 54 | public void Add() => _cache?.AddOrUpdate(_items); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/DynamicData.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0-windows 6 | AnyCPU 7 | false 8 | ;1591;1701;1702;1705;CA1822;CA1001 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/List/DisposeMany_List.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive.Disposables; 7 | 8 | using BenchmarkDotNet.Attributes; 9 | 10 | namespace DynamicData.Benchmarks.List 11 | { 12 | [MemoryDiagnoser] 13 | [MarkdownExporterAttribute.GitHub] 14 | public class DisposeMany_List 15 | { 16 | [Benchmark] 17 | [Arguments(1, 0)] 18 | [Arguments(1, 1)] 19 | [Arguments(10, 0)] 20 | [Arguments(10, 1)] 21 | [Arguments(10, 10)] 22 | [Arguments(100, 0)] 23 | [Arguments(100, 1)] 24 | [Arguments(100, 10)] 25 | [Arguments(100, 100)] 26 | [Arguments(1_000, 0)] 27 | [Arguments(1_000, 1)] 28 | [Arguments(1_000, 10)] 29 | [Arguments(1_000, 100)] 30 | [Arguments(1_000, 1_000)] 31 | public void AddsRemovesAndFinalization(int addCount, int removeCount) 32 | { 33 | using var source = new SourceList(); 34 | 35 | using var subscription = source 36 | .Connect() 37 | .DisposeMany() 38 | .Subscribe(); 39 | 40 | for(var i = 0; i < addCount; ++i) 41 | source.Add(Disposable.Create(static () => { })); 42 | 43 | while(source.Count > (addCount - removeCount)) 44 | source.RemoveAt(source.Count - 1); 45 | 46 | subscription.Dispose(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/List/SourceList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Linq; 6 | 7 | using BenchmarkDotNet.Attributes; 8 | using BenchmarkDotNet.Jobs; 9 | 10 | namespace DynamicData.Benchmarks.List 11 | { 12 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 13 | [MemoryDiagnoser] 14 | [MarkdownExporterAttribute.GitHub] 15 | public class SourceList 16 | { 17 | private SourceList? _sourceList; 18 | private string[]? _items; 19 | 20 | [Params(1, 100, 1_000, 10_000, 100_000)] 21 | public int N; 22 | 23 | [GlobalSetup] 24 | public void Setup() => _sourceList = new SourceList(); 25 | 26 | [IterationSetup] 27 | public void SetupIteration() 28 | { 29 | _sourceList?.Clear(); 30 | _items = Enumerable.Range(1, N).Select(i => i.ToString()).ToArray(); 31 | } 32 | 33 | [GlobalCleanup] 34 | public void Teardown() => _sourceList = null; 35 | 36 | [Benchmark] 37 | public void AddRange() => _sourceList?.AddRange(_items!); 38 | 39 | [Benchmark] 40 | public void Insert() => _sourceList?.InsertRange(_items!, 0); 41 | } 42 | } -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/List/ToObservableChangeSet_List.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Subjects; 3 | 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Columns; 6 | 7 | namespace DynamicData.Benchmarks.List; 8 | 9 | [MemoryDiagnoser] 10 | [MarkdownExporterAttribute.GitHub] 11 | [HideColumns(Column.Ratio, Column.RatioSD, Column.AllocRatio)] 12 | public class ToObservableChangeSet_List 13 | { 14 | [Benchmark] 15 | [Arguments(0, -1)] 16 | [Arguments(0, 0)] 17 | [Arguments(0, 1)] 18 | [Arguments(1, 1)] 19 | [Arguments(10, -1)] 20 | [Arguments(10, 1)] 21 | [Arguments(10, 5)] 22 | [Arguments(10, 10)] 23 | [Arguments(100, -1)] 24 | [Arguments(100, 10)] 25 | [Arguments(100, 50)] 26 | [Arguments(100, 100)] 27 | [Arguments(1_000, -1)] 28 | [Arguments(1_000, 100)] 29 | [Arguments(1_000, 500)] 30 | [Arguments(1_000, 1_000)] 31 | public void AddsUpdatesAndFinalization(int itemCount, int sizeLimit) 32 | { 33 | using var source = new Subject(); 34 | 35 | using var subscription = source 36 | .ToObservableChangeSet(limitSizeTo: sizeLimit) 37 | .Subscribe(); 38 | 39 | var indexModulus = (itemCount / 2) + 1; 40 | for(var i = 0; i < itemCount; ++i) 41 | source.OnNext(i); 42 | source.OnCompleted(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/Polyfills/CompilerFeatureRequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace System.Runtime.CompilerServices; 6 | 7 | // Allows use of the C#11 `required` keyword, internally within this library, when targeting frameworks older than .NET 7. 8 | [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] 9 | internal sealed class CompilerFeatureRequiredAttribute(string featureName) 10 | : Attribute 11 | { 12 | public const string RefStructs 13 | = nameof(RefStructs); 14 | 15 | public const string RequiredMembers 16 | = nameof(RequiredMembers); 17 | 18 | public string FeatureName { get; } = featureName; 19 | 20 | public bool IsOptional { get; init; } 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/Polyfills/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace System.Runtime.CompilerServices; 6 | 7 | // Allows use of the C#11 `init` keyword, internally within this library, when targeting frameworks older than .NET 5. 8 | internal sealed class IsExternalInit 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/Polyfills/RequiredMemberAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | 7 | namespace System.Runtime.CompilerServices; 8 | 9 | // Allows use of the C#11 `required` keyword, internally within this library, when targeting frameworks older than .NET 7. 10 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] 11 | [EditorBrowsable(EditorBrowsableState.Never)] 12 | internal sealed class RequiredMemberAttribute 13 | : Attribute 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/DynamicData.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2019 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.IO; 6 | using System.Runtime.CompilerServices; 7 | 8 | using BenchmarkDotNet.Configs; 9 | using BenchmarkDotNet.Running; 10 | 11 | namespace DynamicData.Benchmarks 12 | { 13 | public static class Program 14 | { 15 | public static void Main(string[] args) 16 | => BenchmarkSwitcher 17 | .FromAssembly(typeof(Program).Assembly) 18 | .Run(args, DefaultConfig.Instance 19 | .WithArtifactsPath(Path.Combine( 20 | GetProjectRootDirectory(), 21 | Path.GetFileName(DefaultConfig.Instance.ArtifactsPath)))); 22 | 23 | private static string GetProjectRootDirectory([CallerFilePath] string? callerFilePath = null) 24 | => Path.GetDirectoryName(callerFilePath)!; 25 | } 26 | } -------------------------------------------------------------------------------- /src/DynamicData.Tests/API/ApiApprovalTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace DynamicData.APITests 6 | { 7 | /// 8 | /// Tests for handling API approval. 9 | /// 10 | [ExcludeFromCodeCoverage] 11 | public class ApiApprovalTests 12 | { 13 | /// 14 | /// Tests to make sure the API of DynamicData project is approved. 15 | /// 16 | [Fact] 17 | public Task DynamicDataTests() => typeof(VirtualRequest).Assembly.CheckApproval(["DynamicData"]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/API/ApiExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Reflection; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading.Tasks; 6 | using PublicApiGenerator; 7 | using VerifyXunit; 8 | 9 | namespace DynamicData.APITests; 10 | 11 | /// 12 | /// A helper for doing API approvals. 13 | /// 14 | [ExcludeFromCodeCoverage] 15 | public static class ApiExtensions 16 | { 17 | /// 18 | /// Checks to make sure the API is approved. 19 | /// 20 | /// The assembly that is being checked. 21 | /// The namespaces. 22 | /// The caller file path. 23 | /// 24 | /// A Task. 25 | /// 26 | public static async Task CheckApproval(this Assembly assembly, string[] namespaces, [CallerFilePath] string filePath = "") 27 | { 28 | var generatorOptions = new ApiGeneratorOptions { AllowNamespacePrefixes = namespaces }; 29 | var apiText = assembly.GeneratePublicApi(generatorOptions); 30 | var result = await Verifier.Verify(apiText, null, filePath) 31 | .UniqueForRuntimeAndVersion() 32 | .ScrubEmptyLines() 33 | .ScrubLines(l => 34 | l.StartsWith("[assembly: AssemblyVersion(", StringComparison.InvariantCulture) || 35 | l.StartsWith("[assembly: AssemblyFileVersion(", StringComparison.InvariantCulture) || 36 | l.StartsWith("[assembly: AssemblyInformationalVersion(", StringComparison.InvariantCulture) || 37 | l.StartsWith("[assembly: System.Reflection.AssemblyMetadata(", StringComparison.InvariantCulture)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Cache/BatchFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DynamicData.Tests.Domain; 4 | 5 | using FluentAssertions; 6 | 7 | using Microsoft.Reactive.Testing; 8 | 9 | using Xunit; 10 | 11 | namespace DynamicData.Tests.Cache; 12 | 13 | public class BatchFixture : IDisposable 14 | { 15 | private readonly ChangeSetAggregator _results; 16 | 17 | private readonly TestScheduler _scheduler; 18 | 19 | private readonly ISourceCache _source; 20 | 21 | public BatchFixture() 22 | { 23 | _scheduler = new TestScheduler(); 24 | _source = new SourceCache(p => p.Key); 25 | _results = _source.Connect().Batch(TimeSpan.FromMinutes(1), _scheduler).AsAggregator(); 26 | } 27 | 28 | public void Dispose() 29 | { 30 | _results.Dispose(); 31 | _source.Dispose(); 32 | } 33 | 34 | [Fact] 35 | public void NoResultsWillBeReceivedBeforeClosingBuffer() 36 | { 37 | _source.AddOrUpdate(new Person("A", 1)); 38 | _results.Messages.Count.Should().Be(0, "There should be no messages"); 39 | } 40 | 41 | [Fact] 42 | public void ResultsWillBeReceivedAfterClosingBuffer() 43 | { 44 | _source.AddOrUpdate(new Person("A", 1)); 45 | 46 | //go forward an arbitary amount of time 47 | _scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); 48 | _results.Messages.Count.Should().Be(1, "Should be 1 update"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Cache/BufferInitialFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using DynamicData.Tests.Domain; 6 | 7 | using FluentAssertions; 8 | 9 | using Microsoft.Reactive.Testing; 10 | 11 | using Xunit; 12 | 13 | namespace DynamicData.Tests.Cache; 14 | 15 | public class BufferInitialFixture 16 | { 17 | private static readonly ICollection People = Enumerable.Range(1, 10_000).Select(i => new Person(i.ToString(), i)).ToList(); 18 | 19 | [Fact] 20 | public void BufferInitial() 21 | { 22 | var scheduler = new TestScheduler(); 23 | 24 | using var cache = new SourceCache(i => i.Name); 25 | using var aggregator = cache.Connect().BufferInitial(TimeSpan.FromSeconds(1), scheduler).AsAggregator(); 26 | foreach (var item in People) 27 | { 28 | cache.AddOrUpdate(item); 29 | } 30 | 31 | aggregator.Data.Count.Should().Be(0); 32 | aggregator.Messages.Count.Should().Be(0); 33 | 34 | scheduler.Start(); 35 | 36 | aggregator.Data.Count.Should().Be(10_000); 37 | aggregator.Messages.Count.Should().Be(1); 38 | 39 | cache.AddOrUpdate(new Person("_New", 1)); 40 | 41 | aggregator.Data.Count.Should().Be(10_001); 42 | aggregator.Messages.Count.Should().Be(2); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Cache/ExpireAfterFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Concurrency; 3 | 4 | using Microsoft.Reactive.Testing; 5 | 6 | namespace DynamicData.Tests.Cache; 7 | 8 | public static partial class ExpireAfterFixture 9 | { 10 | private static TestScheduler CreateTestScheduler() 11 | { 12 | var scheduler = new TestScheduler(); 13 | scheduler.AdvanceTo(DateTimeOffset.FromUnixTimeMilliseconds(0).Ticks); 14 | 15 | return scheduler; 16 | } 17 | 18 | private static Func CreateTimeSelector(IScheduler scheduler) 19 | => item => item.Expiration - scheduler.Now; 20 | 21 | private sealed class TestItem 22 | { 23 | public required int Id { get; init; } 24 | 25 | public DateTimeOffset? Expiration { get; set; } 26 | } 27 | 28 | private sealed record StressItem 29 | { 30 | public required int Id { get; init; } 31 | 32 | public required TimeSpan? Lifetime { get; init; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Cache/FilterOnConnectFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace DynamicData.Tests.Cache; 6 | 7 | /// 8 | /// See https://github.com/reactivemarbles/DynamicData/issues/400 9 | /// 10 | public class FilterOnConnectFixture 11 | { 12 | [Fact] 13 | public void ClearingSourceCacheWithPredicateShouldClearTheData() 14 | { 15 | // having 16 | var source = new SourceCache(it => it); 17 | source.AddOrUpdate(1); 18 | var results = source.Connect(it => true).AsAggregator(); 19 | 20 | // when 21 | source.Clear(); 22 | 23 | // then 24 | results.Data.Count.Should().Be(0, "Should be 0"); 25 | } 26 | 27 | [Fact] 28 | public void UpdatesExistedBeforeConnectWithoutPredicateShouldBeVisibleAsPreviousWhenNewUpdatesTriggered() 29 | { 30 | // having 31 | var source = new SourceCache(it => it); 32 | source.AddOrUpdate(1); 33 | var results = source.Connect().AsAggregator(); 34 | 35 | // when 36 | source.AddOrUpdate(1); 37 | 38 | // then 39 | results.Messages.Count.Should().Be(2, "Should be 2 updates"); 40 | results.Messages[1].First().Previous.HasValue.Should().Be(true, "Should have previous value"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Cache/ForEachChangeFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using DynamicData.Tests.Domain; 5 | 6 | using FluentAssertions; 7 | 8 | using Xunit; 9 | 10 | namespace DynamicData.Tests.Cache; 11 | 12 | public class ForEachChangeFixture : IDisposable 13 | { 14 | private readonly ISourceCache _source; 15 | 16 | public ForEachChangeFixture() => _source = new SourceCache(p => p.Name); 17 | 18 | public void Dispose() => _source.Dispose(); 19 | 20 | [Fact] 21 | public void Test() 22 | { 23 | var messages = new List>(); 24 | var messageWriter = _source.Connect().ForEachChange(messages.Add).Subscribe(); 25 | 26 | _source.AddOrUpdate(new RandomPersonGenerator().Take(100)); 27 | messageWriter.Dispose(); 28 | 29 | messages.Count.Should().Be(100); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Cache/IgnoreUpdateFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DynamicData.Tests.Domain; 4 | 5 | using FluentAssertions; 6 | 7 | using Xunit; 8 | 9 | namespace DynamicData.Tests.Cache; 10 | 11 | public class IgnoreUpdateFixture : IDisposable 12 | { 13 | private readonly ChangeSetAggregator _results; 14 | 15 | private readonly ISourceCache _source; 16 | 17 | public IgnoreUpdateFixture() 18 | { 19 | _source = new SourceCache(p => p.Key); 20 | _results = new ChangeSetAggregator(_source.Connect().IgnoreUpdateWhen((current, previous) => current == previous)); 21 | } 22 | 23 | public void Dispose() 24 | { 25 | _source.Dispose(); 26 | _results.Dispose(); 27 | } 28 | 29 | [Fact] 30 | public void IgnoreFunctionWillIgnoreSubsequentUpdatesOfAnItem() 31 | { 32 | var person = new Person("Person", 10); 33 | _source.AddOrUpdate(person); 34 | _source.AddOrUpdate(person); 35 | _source.AddOrUpdate(person); 36 | 37 | _results.Messages.Count.Should().Be(1, "Should be 1 updates"); 38 | _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Cache/IncludeUpdateFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DynamicData.Tests.Domain; 4 | 5 | using FluentAssertions; 6 | 7 | using Xunit; 8 | 9 | namespace DynamicData.Tests.Cache; 10 | 11 | public class IncludeUpdateFixture : IDisposable 12 | { 13 | private readonly ChangeSetAggregator _results; 14 | 15 | private readonly ISourceCache _source; 16 | 17 | public IncludeUpdateFixture() 18 | { 19 | _source = new SourceCache(p => p.Key); 20 | _results = new ChangeSetAggregator(_source.Connect().IncludeUpdateWhen((current, previous) => current != previous)); 21 | } 22 | 23 | public void Dispose() 24 | { 25 | _source.Dispose(); 26 | _results.Dispose(); 27 | } 28 | 29 | [Fact] 30 | public void IgnoreFunctionWillIgnoreSubsequentUpdatesOfAnItem() 31 | { 32 | var person = new Person("Person", 10); 33 | _source.AddOrUpdate(person); 34 | _source.AddOrUpdate(person); 35 | _source.AddOrUpdate(person); 36 | 37 | _results.Messages.Count.Should().Be(1, "Should be 1 updates"); 38 | _results.Data.Count.Should().Be(1, "Should be 1 item in the cache"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Cache/KeyValueCollectionEx.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace DynamicData.Tests.Cache; 5 | 6 | public static class KeyValueCollectionEx 7 | { 8 | public static IDictionary> Indexed(this IKeyValueCollection source) 9 | where TKey : notnull => source.Select((kv, idx) => new IndexedItem(kv.Value, kv.Key, idx)).ToDictionary(i => i.Key); 10 | } 11 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Domain/ParentChild.cs: -------------------------------------------------------------------------------- 1 | namespace DynamicData.Tests.Domain; 2 | 3 | public class ParentChild(Person child, Person parent) 4 | { 5 | public Person Child { get; } = child; 6 | 7 | public Person Parent { get; } = parent; 8 | } 9 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Domain/PersonWithChildren.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace DynamicData.Tests.Domain; 5 | 6 | public class PersonWithChildren : IKey 7 | { 8 | public PersonWithChildren(string name, int age) 9 | : this(name, age, Enumerable.Empty()) 10 | { 11 | } 12 | 13 | public PersonWithChildren(string name, int age, IEnumerable relations) 14 | { 15 | Name = name; 16 | Age = age; 17 | KeyValue = Name; 18 | Relations = relations; 19 | Key = name; 20 | } 21 | 22 | public int Age { get; set; } 23 | 24 | /// 25 | /// The key 26 | /// 27 | public string Key { get; } 28 | 29 | public string KeyValue { get; } 30 | 31 | public string Name { get; } 32 | 33 | public IEnumerable Relations { get; } 34 | 35 | public override string ToString() => $"{Name}. {Age}"; 36 | } 37 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Domain/PersonWithEmployment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DynamicData.Tests.Domain; 4 | 5 | public class PersonWithEmployment(IGroup source) : IDisposable 6 | { 7 | private readonly IGroup _source = source; 8 | 9 | public int EmploymentCount => EmploymentData.Count; 10 | 11 | public IObservableCache EmploymentData { get; } = source?.Cache!; 12 | 13 | public string Person => _source.Key; 14 | 15 | public void Dispose() => EmploymentData.Dispose(); 16 | 17 | public override string ToString() => $"Person: {Person}. Count {EmploymentCount}"; 18 | } 19 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Domain/PersonWithFriends.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | using DynamicData.Binding; 5 | 6 | namespace DynamicData.Tests.Domain; 7 | 8 | public class PersonWithFriends(string name, int age, IEnumerable friends) : AbstractNotifyPropertyChanged, IKey 9 | { 10 | private int _age = age; 11 | 12 | private IEnumerable _friends = friends; 13 | 14 | public PersonWithFriends(string name, int age) 15 | : this(name, age, Enumerable.Empty()) 16 | { 17 | } 18 | 19 | public int Age 20 | { 21 | get => _age; 22 | set => SetAndRaise(ref _age, value); 23 | } 24 | 25 | public IEnumerable Friends 26 | { 27 | get => _friends; 28 | set => SetAndRaise(ref _friends, value); 29 | } 30 | 31 | /// 32 | /// The key 33 | /// 34 | public string Key { get; } = name; 35 | 36 | public string Name { get; } = name; 37 | 38 | public override string ToString() => $"{Name}. {Age}"; 39 | } 40 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Domain/PersonWithRelations.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace DynamicData.Tests.Domain; 5 | 6 | public class PersonWithRelations : IKey 7 | { 8 | public PersonWithRelations(string name, int age) 9 | : this(name, age, Enumerable.Empty()) 10 | { 11 | } 12 | 13 | public PersonWithRelations(string name, int age, IEnumerable relations) 14 | { 15 | Name = name; 16 | Age = age; 17 | KeyValue = Name; 18 | Relations = relations; 19 | Key = name; 20 | Pet = Enumerable.Empty(); 21 | } 22 | 23 | public int Age { get; } 24 | 25 | /// 26 | /// The key 27 | /// 28 | public string Key { get; } 29 | 30 | public string KeyValue { get; } 31 | 32 | public string Name { get; } 33 | 34 | public IEnumerable Pet { get; set; } 35 | 36 | public IEnumerable Relations { get; } 37 | 38 | public override string ToString() => $"{Name}. {Age}"; 39 | } 40 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Domain/Pet.cs: -------------------------------------------------------------------------------- 1 | namespace DynamicData.Tests.Domain; 2 | 3 | public class Pet 4 | { 5 | public string? Animal { get; set; } 6 | 7 | public string? Name { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Domain/SelfObservingPerson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | 4 | namespace DynamicData.Tests.Domain; 5 | 6 | public class SelfObservingPerson : IDisposable 7 | { 8 | private readonly IDisposable _cleanUp; 9 | 10 | public SelfObservingPerson(IObservable observable) => _cleanUp = observable.Finally(() => Completed = true).Subscribe( 11 | p => 12 | { 13 | Person = p; 14 | UpdateCount++; 15 | }); 16 | 17 | public bool Completed { get; private set; } 18 | 19 | public Person? Person { get; private set; } 20 | 21 | public int UpdateCount { get; private set; } 22 | 23 | /// 24 | ///put here the code to dispose all managed and unmanaged resources 25 | /// 26 | public void Dispose() => _cleanUp.Dispose(); 27 | } 28 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/DynamicData.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | $(NoWarn);CS0618;CA1801;CA1812;CA1816;CA1062;CA1063;CS8767;CS8602;CS8618;IDE1006 5 | 2.* 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Issues/EmptyToChangeSetIssue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Reactive; 3 | using DynamicData.Binding; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace DynamicData.Tests.Issues 8 | { 9 | public class EmptyToChangeSetIssue 10 | { 11 | [Fact] 12 | public void EmptyCollectionToChangeSetBehaviour() 13 | { 14 | var collection = new ObservableCollection(); 15 | 16 | var results = collection.ToObservableChangeSet().AsAggregator(); 17 | results.Messages.Count.Should() 18 | .BeGreaterThan(0, "An empty collection should still have an update, even if empty."); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Issues/Readme.txt: -------------------------------------------------------------------------------- 1 | This is a dumping ground for issue replication 2 | 3 | I am happy for these to be checked in until tests are moved to a more appropriate place. 4 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Kernal/KeyValueFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using DynamicData.Tests.Domain; 4 | 5 | using FluentAssertions; 6 | 7 | using Xunit; 8 | 9 | namespace DynamicData.Tests.Kernal; 10 | 11 | public class KeyValueFixture 12 | { 13 | [Fact] 14 | public void Create() 15 | { 16 | var person = new Person("Person", 10); 17 | var kv = new KeyValuePair("Person", person); 18 | 19 | kv.Key.Should().Be("Person"); 20 | kv.Value.Should().Be(person); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/BatchFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | 4 | using DynamicData.Tests.Domain; 5 | 6 | using FluentAssertions; 7 | 8 | using Microsoft.Reactive.Testing; 9 | 10 | using Xunit; 11 | 12 | namespace DynamicData.Tests.List; 13 | 14 | public class BatchFixture : IDisposable 15 | { 16 | private readonly ChangeSetAggregator _results; 17 | 18 | private readonly TestScheduler _scheduler; 19 | 20 | private readonly ISourceList _source; 21 | 22 | public BatchFixture() 23 | { 24 | _scheduler = new TestScheduler(); 25 | _source = new SourceList(); 26 | _results = _source.Connect().Buffer(TimeSpan.FromMinutes(1), _scheduler).FlattenBufferResult().AsAggregator(); 27 | } 28 | 29 | public void Dispose() 30 | { 31 | _results.Dispose(); 32 | _source.Dispose(); 33 | } 34 | 35 | [Fact] 36 | public void NoResultsWillBeReceivedBeforeClosingBuffer() 37 | { 38 | _source.Add(new Person("A", 1)); 39 | _results.Messages.Count.Should().Be(0, "There should be no messages"); 40 | } 41 | 42 | [Fact] 43 | public void ResultsWillBeReceivedAfterClosingBuffer() 44 | { 45 | _source.Add(new Person("A", 1)); 46 | 47 | //go forward an arbitary amount of time 48 | _scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); 49 | _results.Messages.Count.Should().Be(1, "Should be 1 update"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/BufferFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | 4 | using DynamicData.Tests.Domain; 5 | 6 | using FluentAssertions; 7 | 8 | using Microsoft.Reactive.Testing; 9 | 10 | using Xunit; 11 | 12 | namespace DynamicData.Tests.List; 13 | 14 | public class BufferFixture : IDisposable 15 | { 16 | private readonly ChangeSetAggregator _results; 17 | 18 | private readonly TestScheduler _scheduler; 19 | 20 | private readonly ISourceList _source; 21 | 22 | public BufferFixture() 23 | { 24 | _scheduler = new TestScheduler(); 25 | _source = new SourceList(); 26 | _results = _source.Connect().Buffer(TimeSpan.FromMinutes(1), _scheduler).FlattenBufferResult().AsAggregator(); 27 | } 28 | 29 | public void Dispose() 30 | { 31 | _results.Dispose(); 32 | _source.Dispose(); 33 | } 34 | 35 | [Fact] 36 | public void NoResultsWillBeReceivedBeforeClosingBuffer() 37 | { 38 | _source.Add(new Person("A", 1)); 39 | _results.Messages.Count.Should().Be(0, "There should be no messages"); 40 | } 41 | 42 | [Fact] 43 | public void ResultsWillBeReceivedAfterClosingBuffer() 44 | { 45 | _source.Add(new Person("A", 1)); 46 | 47 | //go forward an arbitary amount of time 48 | _scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks); 49 | _results.Messages.Count.Should().Be(1, "Should be 1 update"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/BufferInitialFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using DynamicData.Tests.Domain; 6 | 7 | using FluentAssertions; 8 | 9 | using Microsoft.Reactive.Testing; 10 | 11 | using Xunit; 12 | 13 | namespace DynamicData.Tests.List; 14 | 15 | public class BufferInitialFixture 16 | { 17 | private static readonly ICollection People = Enumerable.Range(1, 10_000).Select(i => new Person(i.ToString(), i)).ToList(); 18 | 19 | [Fact] 20 | public void BufferInitial() 21 | { 22 | var scheduler = new TestScheduler(); 23 | 24 | using var cache = new SourceList(); 25 | using var aggregator = cache.Connect().BufferInitial(TimeSpan.FromSeconds(1), scheduler).AsAggregator(); 26 | foreach (var item in People) 27 | { 28 | cache.Add(item); 29 | } 30 | 31 | aggregator.Data.Count.Should().Be(0); 32 | aggregator.Messages.Count.Should().Be(0); 33 | 34 | scheduler.Start(); 35 | 36 | aggregator.Data.Count.Should().Be(10_000); 37 | aggregator.Messages.Count.Should().Be(1); 38 | 39 | cache.Add(new Person("_New", 1)); 40 | 41 | aggregator.Data.Count.Should().Be(10_001); 42 | aggregator.Messages.Count.Should().Be(2); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/CastFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using FluentAssertions; 5 | 6 | using Xunit; 7 | 8 | namespace DynamicData.Tests.List; 9 | 10 | public class CastFixture : IDisposable 11 | { 12 | private readonly ChangeSetAggregator _results; 13 | 14 | private readonly ISourceList _source; 15 | 16 | public CastFixture() 17 | { 18 | _source = new SourceList(); 19 | _results = _source.Cast(i => (decimal)i).AsAggregator(); 20 | } 21 | 22 | [Fact] 23 | public void CanCast() 24 | { 25 | _source.AddRange(Enumerable.Range(1, 10)); 26 | _results.Data.Count.Should().Be(10); 27 | 28 | _source.Clear(); 29 | _results.Data.Count.Should().Be(0); 30 | } 31 | 32 | public void Dispose() 33 | { 34 | _source.Dispose(); 35 | _results.Dispose(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/CreationFixtures.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using System.Threading.Tasks; 4 | 5 | using FluentAssertions; 6 | 7 | using Xunit; 8 | 9 | namespace DynamicData.Tests.List; 10 | 11 | public class ListCreationFixtures 12 | { 13 | [Fact] 14 | public void Create() 15 | { 16 | static Task CreateTask(T value) => Task.FromResult(value); 17 | 18 | SubscribeAndAssert( 19 | ObservableChangeSet.Create( 20 | async list => 21 | { 22 | var value = await CreateTask(10); 23 | list.Add(value); 24 | return () => { }; 25 | })); 26 | } 27 | 28 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Accetable for test.")] 29 | private void SubscribeAndAssert(IObservable> observableChangeset, bool expectsError = false) 30 | where T : notnull 31 | { 32 | Exception? error = null; 33 | var complete = false; 34 | IChangeSet? changes = null; 35 | 36 | using (var myList = observableChangeset.Finally(() => complete = true).AsObservableList()) 37 | using (myList.Connect().Subscribe(result => changes = result, ex => error = ex)) 38 | { 39 | if (!expectsError) 40 | { 41 | error.Should().BeNull(); 42 | } 43 | else 44 | { 45 | error.Should().NotBeNull(); 46 | } 47 | } 48 | 49 | complete.Should().BeTrue(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/MergeManyChangeSetsFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using FluentAssertions; 3 | 4 | using Xunit; 5 | 6 | namespace DynamicData.Tests.List; 7 | 8 | public class MergeManyChangeSetsFixture 9 | { 10 | [Fact] 11 | public void MergeManyShouldWork() 12 | { 13 | var a = new SourceList(); 14 | var b = new SourceList(); 15 | var c = new SourceList(); 16 | 17 | var parent = new SourceList>(); 18 | parent.Add(a); 19 | parent.Add(b); 20 | parent.Add(c); 21 | 22 | var d = parent.Connect().MergeMany(e => e.Connect().RemoveIndex()).AsObservableList(); 23 | 24 | d.Count.Should().Be(0); 25 | 26 | a.Add(1); 27 | 28 | d.Count.Should().Be(1); 29 | a.Add(2); 30 | d.Count.Should().Be(2); 31 | 32 | b.Add(3); 33 | d.Count.Should().Be(3); 34 | b.Add(5); 35 | d.Count.Should().Be(4); 36 | new[] { 1, 2, 3, 5 }.Should().BeEquivalentTo(d.Items); 37 | 38 | b.Clear(); 39 | 40 | // Fails below 41 | d.Count.Should().Be(2); 42 | new[] { 1, 2 }.Should().BeEquivalentTo(d.Items); 43 | 44 | a.ReplaceAt(0,100); 45 | new[] { 2, 100 }.Should().BeEquivalentTo(d.Items); 46 | 47 | 48 | var f = new SourceList(); 49 | f.AddRange(Enumerable.Range(10,5)); 50 | parent.ReplaceAt(2,f); 51 | 52 | 53 | new[] { 2, 100, 10,11,12,13,14 }.Should().BeEquivalentTo(d.Items); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/OnItemFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DynamicData.Tests.Domain; 4 | 5 | using Xunit; 6 | 7 | namespace DynamicData.Tests.List; 8 | 9 | public class OnItemFixture 10 | { 11 | [Fact] 12 | public void OnItemAddCalled() 13 | { 14 | var called = false; 15 | var source = new SourceList(); 16 | 17 | source.Connect().OnItemAdded(_ => called = true).Subscribe(); 18 | 19 | var person = new Person("A", 1); 20 | 21 | source.Add(person); 22 | Assert.True(called); 23 | } 24 | 25 | [Fact] 26 | public void OnItemRefreshedCalled() 27 | { 28 | var called = false; 29 | var source = new SourceList(); 30 | 31 | var person = new Person("A", 1); 32 | source.Add(person); 33 | 34 | source.Connect().AutoRefresh(x=>x.Age).OnItemRefreshed(_ => called = true).Subscribe(); 35 | 36 | person.Age += 1; 37 | 38 | Assert.True(called); 39 | } 40 | 41 | [Fact] 42 | public void OnItemRemovedCalled() 43 | { 44 | var called = false; 45 | var source = new SourceList(); 46 | 47 | source.Connect().OnItemRemoved(_ => called = true).Subscribe(); 48 | 49 | var person = new Person("A", 1); 50 | source.Add(person); 51 | source.Remove(person); 52 | Assert.True(called); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/RemoveManyFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using FluentAssertions; 6 | 7 | using Xunit; 8 | 9 | namespace DynamicData.Tests.List; 10 | 11 | public class RemoveManyFixture 12 | { 13 | private readonly List _list; 14 | 15 | public RemoveManyFixture() => _list = new List(); 16 | 17 | [Fact] 18 | public void DoesNotRemoveDuplicates() 19 | { 20 | _list.AddRange(new[] { 1, 1, 1, 5, 6, 7 }); 21 | _list.RemoveMany(new[] { 1, 1, 7 }); 22 | _list.Should().BeEquivalentTo(new[] { 1, 5, 6 }); 23 | } 24 | 25 | [Fact] 26 | public void RemoveLargeBatch() 27 | { 28 | var toAdd = Enumerable.Range(1, 10000).ToArray(); 29 | _list.AddRange(toAdd); 30 | 31 | var toRemove = _list.Take(_list.Count / 2).OrderBy(x => Guid.NewGuid()).ToArray(); 32 | _list.RemoveMany(toRemove); 33 | _list.Should().BeEquivalentTo(toAdd.Except(toRemove)); 34 | } 35 | 36 | [Fact] 37 | public void RemoveManyWillRemoveARange() 38 | { 39 | _list.AddRange(Enumerable.Range(1, 10)); 40 | _list.RemoveMany(Enumerable.Range(2, 8)); 41 | _list.Should().BeEquivalentTo(new[] { 1, 10 }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/SortPrimitiveFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using DynamicData.Binding; 6 | 7 | using FluentAssertions; 8 | 9 | using Xunit; 10 | 11 | namespace DynamicData.Tests.List; 12 | 13 | public class SortPrimitiveFixture : IDisposable 14 | { 15 | private readonly IComparer _comparer = SortExpressionComparer.Ascending(i => i); 16 | 17 | private readonly ChangeSetAggregator _results; 18 | 19 | private readonly ISourceList _source; 20 | 21 | public SortPrimitiveFixture() 22 | { 23 | _source = new SourceList(); 24 | _results = _source.Connect().Sort(_comparer).AsAggregator(); 25 | } 26 | 27 | public void Dispose() 28 | { 29 | _results.Dispose(); 30 | _source.Dispose(); 31 | } 32 | 33 | [Fact] 34 | public void RemoveRandomSorts() 35 | { 36 | //seems an odd test but believe me it catches exceptions when sorting on primitives 37 | var items = Enumerable.Range(1, 100).OrderBy(_ => Guid.NewGuid()).ToArray(); 38 | _source.AddRange(items); 39 | 40 | _results.Data.Count.Should().Be(100); 41 | 42 | var expectedResult = items.OrderBy(p => p, _comparer); 43 | var actualResult = _results.Data.Items; 44 | 45 | actualResult.Should().BeEquivalentTo(expectedResult); 46 | 47 | for (var i = 0; i < 50; i++) 48 | { 49 | _source.Remove(i); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/SourceListFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace DynamicData.Tests.List; 8 | 9 | public class SourceListFixture 10 | { 11 | [Fact] 12 | public void InitialChangeIsRange() 13 | { 14 | var source = new SourceList(); 15 | source.Add("A"); 16 | var changeSets = new List>(); 17 | 18 | source.Connect().Subscribe(changeSets.Add).Dispose(); 19 | 20 | 21 | changeSets[0].First().Type.Should().Be(ChangeType.Range); 22 | changeSets[0].First().Range.Index.Should().Be(0); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/List/ToCollectionFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace DynamicData.Tests.List; 8 | 9 | public class ToCollectionFixture 10 | { 11 | [Fact] 12 | public void ToCollectionTest() 13 | { 14 | var list = new SourceList(); 15 | // var collection = Observable.Defer(() => list.Connect().ToCollection()); 16 | var collection = list.Connect().ToCollection(); 17 | IReadOnlyCollection? res1 = null; 18 | IReadOnlyCollection? res2 = null; 19 | collection.Subscribe(x => res1 = x); 20 | collection.Subscribe(x => res2 = x); 21 | list.Add("1"); 22 | list.Add("2"); 23 | res1?.Count.Should().Be(2); 24 | res2?.Count.Should().Be(2); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/ComparerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace DynamicData.Tests.Utilities; 6 | 7 | internal sealed class NoOpComparer : IComparer 8 | { 9 | public int Compare(T x, T y) => throw new NotImplementedException(); 10 | } 11 | 12 | internal sealed class NoOpEqualityComparer : IEqualityComparer 13 | { 14 | [SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Suppressed for Net 9.0")] 15 | public bool Equals(T x, T y) => throw new NotImplementedException(); 16 | [SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Suppressed for Net 9.0")] 17 | public int GetHashCode([DisallowNull] T obj) => throw new NotImplementedException(); 18 | } 19 | 20 | 21 | internal sealed class InvertedComparer(IComparer original) : IComparer 22 | { 23 | private readonly IComparer _original = original; 24 | 25 | public int Compare(T x, T y) => _original.Compare(x, y) * -1; 26 | } 27 | 28 | 29 | internal static class ComparerExtensions 30 | { 31 | public static IComparer Invert(this IComparer comparer) => new InvertedComparer(comparer); 32 | } 33 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/FakerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Concurrency; 5 | using Bogus; 6 | 7 | namespace DynamicData.Tests.Utilities; 8 | 9 | internal static class FakerExtensions 10 | { 11 | public static IObservable IntervalGenerate(this Faker faker, Randomizer randomizer, TimeSpan maxTime, IScheduler? scheduler = null) 12 | where T : class => 13 | randomizer.Interval(maxTime, scheduler).Select(_ => faker.Generate()); 14 | 15 | public static IObservable IntervalGenerate(this Faker faker, TimeSpan period, IScheduler? scheduler = null) 16 | where T : class => 17 | Observable.Interval(period, scheduler ?? DefaultScheduler.Instance).Select(_ => faker.Generate()); 18 | } 19 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/FunctionalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DynamicData.Tests.Utilities; 4 | 5 | internal static class FunctionalExtensions 6 | { 7 | public static T With(this T item, Action action) 8 | { 9 | action(item); 10 | return item; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/ObservableEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Concurrency; 3 | using System.Reactive.Linq; 4 | 5 | namespace DynamicData.Tests.Utilities; 6 | 7 | /// 8 | /// Extra Observable Tools for Testing 9 | /// 10 | internal static class ObservableEx 11 | { 12 | /// 13 | /// Like except the interval time is recomputed each time. 14 | /// 15 | /// Function to get the next interval. 16 | /// Scheduler to use for firing events 17 | /// IObservable{long} instance. 18 | public static IObservable Interval(Func nextInterval, IScheduler? scheduler = null) => 19 | Observable.Create(observer => 20 | { 21 | _ = nextInterval ?? throw new ArgumentNullException(nameof(nextInterval)); 22 | 23 | IDisposable ScheduleFirst(IScheduler sch) 24 | { 25 | IDisposable HandleNext(IScheduler _, long counter) 26 | { 27 | observer.OnNext(counter); 28 | return ScheduleNext(sch, counter + 1); 29 | } 30 | 31 | IDisposable ScheduleNext(IScheduler _, long counter) => sch.Schedule(counter, nextInterval(), HandleNext); 32 | 33 | return sch.Schedule(0, nextInterval(), HandleNext); 34 | } 35 | 36 | return ScheduleFirst(scheduler ?? DefaultScheduler.Instance); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/RandomizerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reactive.Concurrency; 4 | using Bogus; 5 | 6 | namespace DynamicData.Tests.Utilities; 7 | 8 | internal static class RandomizerExtensions 9 | { 10 | public static TimeSpan TimeSpan(this Randomizer randomizer, TimeSpan minTime, TimeSpan maxTime) => System.TimeSpan.FromTicks(randomizer.Long(minTime.Ticks, maxTime.Ticks)); 11 | 12 | public static TimeSpan TimeSpan(this Randomizer randomizer, TimeSpan maxTime) => TimeSpan(randomizer, System.TimeSpan.Zero, maxTime); 13 | 14 | public static bool CoinFlip(this Randomizer randomizer, Action action) 15 | { 16 | if (randomizer.Bool()) 17 | { 18 | action(); 19 | return true; 20 | } 21 | 22 | return false; 23 | } 24 | 25 | public static bool Chance(this Randomizer randomizer, double chancePercent, Action action) 26 | { 27 | Debug.Assert(chancePercent >= 0.0 && chancePercent <= 1.0); 28 | if (randomizer.Double() <= chancePercent) 29 | { 30 | action(); 31 | return true; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | public static IObservable Interval(this Randomizer randomizer, TimeSpan minTime, TimeSpan maxTime, IScheduler? scheduler = null) => 38 | ObservableEx.Interval(() => randomizer.TimeSpan(minTime, maxTime), scheduler); 39 | 40 | public static IObservable Interval(this Randomizer randomizer, TimeSpan maxTime, IScheduler? scheduler = null) => 41 | Interval(randomizer, System.TimeSpan.Zero, maxTime, scheduler); 42 | } 43 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/RawAnonymousObservable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DynamicData.Tests.Utilities; 4 | 5 | internal static class RawAnonymousObservable 6 | { 7 | public static RawAnonymousObservable Create(Func, IDisposable> onSubscribe) 8 | => new(onSubscribe); 9 | } 10 | 11 | // Allows bypassing of safeguards implemented within Observable.Create(), for testing. 12 | internal class RawAnonymousObservable 13 | : IObservable 14 | { 15 | private readonly Func, IDisposable> _onSubscribe; 16 | 17 | public RawAnonymousObservable(Func, IDisposable> onSubscribe) 18 | => _onSubscribe = onSubscribe; 19 | 20 | public IDisposable Subscribe(IObserver observer) 21 | => _onSubscribe.Invoke(observer); 22 | } 23 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/RawAnonymousObserver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DynamicData.Tests.Utilities; 4 | 5 | internal static class RawAnonymousObserver 6 | { 7 | public static RawAnonymousObserver Create( 8 | Action onNext, 9 | Action onError, 10 | Action onCompleted) 11 | => new( 12 | onNext: onNext, 13 | onError: onError, 14 | onCompleted: onCompleted); 15 | } 16 | 17 | // Allows bypassing of safeguards implemented within Observer.Create(), for testing. 18 | internal class RawAnonymousObserver 19 | : IObserver 20 | { 21 | private readonly Action _onCompleted; 22 | private readonly Action _onError; 23 | private readonly Action _onNext; 24 | 25 | public RawAnonymousObserver( 26 | Action onNext, 27 | Action onError, 28 | Action onCompleted) 29 | { 30 | _onNext = onNext; 31 | _onError = onError; 32 | _onCompleted = onCompleted; 33 | } 34 | 35 | public void OnCompleted() 36 | => _onCompleted.Invoke(); 37 | 38 | public void OnError(Exception error) 39 | => _onError.Invoke(error); 40 | 41 | public void OnNext(T value) 42 | => _onNext.Invoke(value); 43 | } 44 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/UnsynchronizedNotificationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive; 3 | 4 | namespace DynamicData.Tests.Utilities; 5 | 6 | public class UnsynchronizedNotificationException 7 | : Exception 8 | { 9 | public UnsynchronizedNotificationException() 10 | : base("Unsynchronized notification received: Another notification is already being processed") 11 | { } 12 | 13 | public required Notification IncomingNotification { get; init; } 14 | 15 | public required Notification PriorNotification { get; init; } 16 | } 17 | -------------------------------------------------------------------------------- /src/DynamicData.Tests/Utilities/ValueRecordingObserver.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reactive.Concurrency; 3 | 4 | namespace DynamicData.Tests.Utilities; 5 | 6 | public sealed class ValueRecordingObserver 7 | : RecordingObserverBase 8 | { 9 | private readonly List _recordedValues; 10 | 11 | public ValueRecordingObserver(IScheduler scheduler) 12 | : base(scheduler) 13 | => _recordedValues = new(); 14 | 15 | public IReadOnlyList RecordedValues 16 | => _recordedValues; 17 | 18 | protected override void OnNext(T value) 19 | { 20 | if (!HasFinalized) 21 | _recordedValues.Add(value); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/Aggregation/AggregateType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Aggregation; 6 | 7 | /// 8 | /// The type of aggregation. 9 | /// 10 | public enum AggregateType 11 | { 12 | /// 13 | /// The add. 14 | /// 15 | Add, 16 | 17 | /// 18 | /// The remove. 19 | /// 20 | Remove 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/Aggregation/Avg.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Aggregation; 6 | 7 | internal readonly struct Avg(int count, TValue sum) 8 | { 9 | public int Count { get; } = count; 10 | 11 | public TValue Sum { get; } = sum; 12 | } 13 | -------------------------------------------------------------------------------- /src/DynamicData/Aggregation/IAggregateChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Aggregation; 6 | 7 | /// 8 | /// A change set which has been shaped for rapid online aggregations. 9 | /// 10 | /// The type of the item. 11 | public interface IAggregateChangeSet : IEnumerable> 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/DynamicData/Aggregation/StdDev.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Aggregation; 6 | 7 | internal readonly struct StdDev(int count, TValue sumOfItems, TValue sumOfSquares) 8 | { 9 | public int Count { get; } = count; 10 | 11 | public TValue SumOfItems { get; } = sumOfItems; 12 | 13 | public TValue SumOfSquares { get; } = sumOfSquares; 14 | } 15 | -------------------------------------------------------------------------------- /src/DynamicData/Attributes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | [assembly: InternalsVisibleTo("DynamicData.Tests")] 8 | [assembly: InternalsVisibleTo("DynamicData.ReactiveUI")] 9 | [assembly: InternalsVisibleTo("DynamicData.Profile")] -------------------------------------------------------------------------------- /src/DynamicData/Binding/BindingListEventsSuspender.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Reactive.Disposables; 8 | 9 | namespace DynamicData.Binding; 10 | 11 | internal sealed class BindingListEventsSuspender<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : IDisposable 12 | { 13 | private readonly IDisposable _cleanUp; 14 | 15 | public BindingListEventsSuspender(BindingList list) 16 | { 17 | list.RaiseListChangedEvents = false; 18 | 19 | _cleanUp = Disposable.Create( 20 | () => 21 | { 22 | list.RaiseListChangedEvents = true; 23 | list.ResetBindings(); 24 | }); 25 | } 26 | 27 | public void Dispose() => _cleanUp.Dispose(); 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/IEvaluateAware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Binding; 6 | 7 | /// 8 | /// Implement on an object and use in conjunction with InvokeEvaluate operator 9 | /// to make an object aware of any evaluates. 10 | /// 11 | public interface IEvaluateAware 12 | { 13 | /// 14 | /// Refresh method. 15 | /// 16 | void Evaluate(); 17 | } 18 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/IIndexAware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Binding; 6 | 7 | /// 8 | /// Implement on an object and use in conjunction with UpdateIndex operator 9 | /// to make an object aware of it's sorted index. 10 | /// 11 | public interface IIndexAware 12 | { 13 | /// 14 | /// Gets or sets the index. 15 | /// 16 | /// 17 | /// The index. 18 | /// 19 | int Index { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/INotifyCollectionChangedSuspender.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Binding; 6 | 7 | /// 8 | /// Represents an observable collection where collection changed and count notifications can be suspended. 9 | /// 10 | public interface INotifyCollectionChangedSuspender 11 | { 12 | /// 13 | /// Suspends count notifications. 14 | /// 15 | /// A disposable which when disposed re-activates count notifications. 16 | IDisposable SuspendCount(); 17 | 18 | /// 19 | /// Suspends notifications. When disposed, a reset notification is fired. 20 | /// 21 | /// A disposable which when disposed re-activates notifications. 22 | IDisposable SuspendNotifications(); 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/IObservableCollection.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Specialized; 6 | using System.ComponentModel; 7 | 8 | namespace DynamicData.Binding; 9 | 10 | /// 11 | /// An override of observable collection which allows the suspension of notifications. 12 | /// 13 | /// The type of the item. 14 | public interface IObservableCollection : INotifyCollectionChanged, INotifyPropertyChanged, IList, INotifyCollectionChangedSuspender 15 | { 16 | /// 17 | /// Clears the list and Loads the specified items. 18 | /// 19 | /// The items. 20 | void Load(IEnumerable items); 21 | 22 | /// 23 | /// Moves the item at the specified index to a new location in the collection. 24 | /// 25 | /// The zero-based index specifying the location of the item to be moved. 26 | /// The zero-based index specifying the new location of the item. 27 | void Move(int oldIndex, int newIndex); 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/IObservableCollectionAdaptor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Binding; 6 | 7 | /// 8 | /// Represents an adaptor which is used to update observable collection from 9 | /// a change set stream. 10 | /// 11 | /// The type of the object. 12 | /// The type of the key. 13 | public interface IObservableCollectionAdaptor 14 | where TObject : notnull 15 | where TKey : notnull 16 | { 17 | /// 18 | /// Maintains the specified collection from the changes. 19 | /// 20 | /// The changes. 21 | /// The collection. 22 | void Adapt(IChangeSet changes, IObservableCollection collection); 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/ISortedObservableCollectionAdaptor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Binding; 6 | 7 | /// 8 | /// Represents an adaptor which is used to update observable collection from 9 | /// a sorted change set stream. 10 | /// 11 | /// The type of the object. 12 | /// The type of the key. 13 | public interface ISortedObservableCollectionAdaptor 14 | where TObject : notnull 15 | where TKey : notnull 16 | { 17 | /// 18 | /// Maintains the specified collection from the changes. 19 | /// 20 | /// The changes. 21 | /// The collection. 22 | void Adapt(ISortedChangeSet changes, IObservableCollection collection); 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/Observable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.Binding; 8 | 9 | internal static class Observable 10 | { 11 | public static IObservable Default { get; } = Observable.Return(default); 12 | 13 | public static IObservable Empty { get; } = Observable.Empty(); 14 | 15 | public static IObservable Never { get; } = Observable.Never(); 16 | } 17 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/ObservablePropertyPart.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Diagnostics; 6 | using System.Linq.Expressions; 7 | using System.Reactive; 8 | 9 | namespace DynamicData.Binding; 10 | 11 | [DebuggerDisplay("ObservablePropertyPart<{" + nameof(expression) + "}>")] 12 | internal sealed class ObservablePropertyPart(MemberExpression expression) 13 | { 14 | public Func Accessor { get; } = expression.CreateValueAccessor(); 15 | 16 | public Func> Factory { get; } = expression.CreatePropertyChangedFactory(); 17 | } 18 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/SortDirection.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Binding; 6 | 7 | /// 8 | /// Sort direction. 9 | /// 10 | public enum SortDirection 11 | { 12 | /// 13 | /// Sort items ascending. 14 | /// 15 | Ascending, 16 | 17 | /// 18 | /// Sort items descending. 19 | /// 20 | Descending 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/Binding/SortExpression.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Binding; 6 | 7 | /// 8 | /// A value expression with sort direction. 9 | /// 10 | /// The type of the item. 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The expression. 15 | /// The direction. 16 | public class SortExpression(Func expression, SortDirection direction = SortDirection.Ascending) 17 | { 18 | /// 19 | /// Gets the direction. 20 | /// 21 | public SortDirection Direction { get; } = direction; 22 | 23 | /// 24 | /// Gets the expression. 25 | /// 26 | public Func Expression { get; } = expression; 27 | } 28 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/ChangeReason.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// The reason for an individual change. 10 | /// Used to signal consumers of any changes to the underlying cache. 11 | /// 12 | public enum ChangeReason 13 | { 14 | /// 15 | /// An item has been added. 16 | /// 17 | Add = 0, 18 | 19 | /// 20 | /// An item has been updated. 21 | /// 22 | Update = 1, 23 | 24 | /// 25 | /// An item has removed. 26 | /// 27 | Remove = 2, 28 | 29 | /// 30 | /// Downstream operators will refresh. 31 | /// 32 | Refresh = 3, 33 | 34 | /// 35 | /// An item has been moved in a sorted collection. 36 | /// 37 | Moved = 4, 38 | } 39 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/DistinctChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | internal sealed class DistinctChangeSet : ChangeSet, IDistinctChangeSet 9 | where T : notnull 10 | { 11 | public DistinctChangeSet(IEnumerable> items) 12 | : base(items) 13 | { 14 | } 15 | 16 | public DistinctChangeSet() 17 | { 18 | } 19 | 20 | public DistinctChangeSet(int capacity) 21 | : base(capacity) 22 | { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/GroupChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | internal sealed class GroupChangeSet : ChangeSet, TGroupKey>, IGroupChangeSet 9 | where TObject : notnull 10 | where TKey : notnull 11 | where TGroupKey : notnull 12 | { 13 | public static new readonly IGroupChangeSet Empty = new GroupChangeSet(); 14 | 15 | public GroupChangeSet(IEnumerable, TGroupKey>> items) 16 | : base(items) 17 | { 18 | } 19 | 20 | private GroupChangeSet() 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// A collection of changes with some arbitrary additional context. 10 | /// Changes are always published in the order. 11 | /// 12 | /// The type of the object. 13 | /// The type of the key. 14 | /// The additional context. 15 | public interface IChangeSet : IChangeSet 16 | where TObject : notnull 17 | where TKey : notnull 18 | { 19 | /// 20 | /// Additional context. 21 | /// 22 | TContext Context { get; } 23 | } 24 | 25 | /// 26 | /// A collection of changes. 27 | /// Changes are always published in the order. 28 | /// 29 | /// The type of the object. 30 | /// The type of the key. 31 | public interface IChangeSet : IChangeSet, IEnumerable> 32 | where TObject : notnull 33 | where TKey : notnull 34 | { 35 | /// 36 | /// Gets the number of updates. 37 | /// 38 | int Updates { get; } 39 | } 40 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IChangeSetAdaptor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// A simple adaptor to inject side effects into a change set observable. 10 | /// 11 | /// The type of the object. 12 | /// The type of the key. 13 | public interface IChangeSetAdaptor 14 | where TObject : notnull 15 | where TKey : notnull 16 | { 17 | /// 18 | /// Adapts the specified change. 19 | /// 20 | /// The change. 21 | void Adapt(IChangeSet changes); 22 | } 23 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IDistinctChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// A collection of distinct value updates. 10 | /// 11 | /// The type of the item. 12 | public interface IDistinctChangeSet : IChangeSet 13 | where T : notnull 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IGroup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// An update stream which has been grouped by a common key. 10 | /// 11 | /// The type of the object. 12 | /// The type of the key. 13 | /// The type of value used to group the original stream. 14 | public interface IGroup : IKey 15 | where TObject : notnull 16 | where TKey : notnull 17 | { 18 | /// 19 | /// Gets the observable for the group. 20 | /// 21 | /// 22 | /// The observable. 23 | /// 24 | IObservableCache Cache { get; } 25 | } 26 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IGroupChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// A grouped change set. 10 | /// 11 | /// The source object type. 12 | /// The type of the key.s 13 | /// The value on which the stream has been grouped. 14 | public interface IGroupChangeSet : IChangeSet, TGroupKey> 15 | where TObject : notnull 16 | where TKey : notnull 17 | where TGroupKey : notnull 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IImmutableGroupChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// A grouped update collection. 9 | /// 10 | /// The source object type. 11 | /// The type of the key.s 12 | /// The value on which the stream has been grouped. 13 | public interface IImmutableGroupChangeSet : IChangeSet, TGroupKey> 14 | where TObject : notnull 15 | where TKey : notnull 16 | where TGroupKey : notnull 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IIntermediateCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// An observable cache which exposes an update API. 10 | /// Intended to be used as a helper for creating custom operators. 11 | /// 12 | /// The type of the object. 13 | /// The type of the key. 14 | public interface IIntermediateCache : IObservableCache 15 | where TObject : notnull 16 | where TKey : notnull 17 | { 18 | /// 19 | /// Action to apply a batch update to a cache. Multiple update methods can be invoked within a single batch operation. 20 | /// These operations are invoked within the cache's lock and is therefore thread safe. 21 | /// The result of the action will produce a single change set. 22 | /// 23 | /// The update action. 24 | void Edit(Action> updateAction); 25 | } 26 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IKey.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Represents the key of an object. 9 | /// 10 | /// The type of the item. 11 | public interface IKey 12 | { 13 | /// 14 | /// Gets the key. 15 | /// 16 | T Key { get; } 17 | } 18 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IKeyValue.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// A keyed value. 9 | /// 10 | /// The type of the object. 11 | /// The type of the key. 12 | public interface IKeyValue : IKey 13 | { 14 | /// 15 | /// Gets the value. 16 | /// 17 | TObject Value { get; } 18 | } 19 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IKeyValueCollection.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// A key collection which contains sorting information. 10 | /// 11 | /// The type of the object. 12 | /// The type of the key. 13 | public interface IKeyValueCollection : IReadOnlyList> 14 | { 15 | /// 16 | /// Gets the comparer used to perform the sort. 17 | /// 18 | /// 19 | /// The comparer. 20 | /// 21 | IComparer> Comparer { get; } 22 | 23 | /// 24 | /// Gets the optimisations used to produce the sort. 25 | /// 26 | /// 27 | /// The optimisations. 28 | /// 29 | SortOptimisations Optimisations { get; } 30 | 31 | /// 32 | /// Gets the reason for a sort being applied. 33 | /// 34 | /// 35 | /// The sort reason. 36 | /// 37 | SortReason SortReason { get; } 38 | } 39 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IObservableCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// A cache for observing and querying in memory data. With additional data access operators. 10 | /// 11 | /// The type of the object. 12 | /// The type of the key. 13 | public interface IObservableCache : IConnectableCache, IDisposable 14 | where TObject : notnull 15 | where TKey : notnull 16 | { 17 | /// 18 | /// Gets the total count of cached items. 19 | /// 20 | int Count { get; } 21 | 22 | /// 23 | /// Gets the Items. 24 | /// 25 | IReadOnlyList Items { get; } 26 | 27 | /// 28 | /// Gets the keys. 29 | /// 30 | IReadOnlyList Keys { get; } 31 | 32 | /// 33 | /// Gets the key value pairs. 34 | /// 35 | IReadOnlyDictionary KeyValues { get; } 36 | 37 | /// 38 | /// Lookup a single item using the specified key. 39 | /// 40 | /// 41 | /// Fast indexed lookup. 42 | /// 43 | /// The key. 44 | /// An optional with the looked up value. 45 | Optional Lookup(TKey key); 46 | } 47 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IPageRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Represents a new page request. 9 | /// 10 | public interface IPageRequest 11 | { 12 | /// 13 | /// Gets the page to move to. 14 | /// 15 | int Page { get; } 16 | 17 | /// 18 | /// Gets the page size. 19 | /// 20 | int Size { get; } 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IPageResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Operators; 6 | 7 | /// 8 | /// Response from the pagination operator. 9 | /// 10 | public interface IPageResponse 11 | { 12 | /// 13 | /// Gets the current page. 14 | /// 15 | int Page { get; } 16 | 17 | /// 18 | /// Gets total number of pages. 19 | /// 20 | int Pages { get; } 21 | 22 | /// 23 | /// Gets the size of the page. 24 | /// 25 | int PageSize { get; } 26 | 27 | /// 28 | /// Gets the total number of records in the underlying cache. 29 | /// 30 | int TotalSize { get; } 31 | } 32 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IPagedChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using DynamicData.Operators; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace DynamicData; 9 | 10 | /// 11 | /// A paged update collection. 12 | /// 13 | /// The type of the object. 14 | /// The type of the key. 15 | public interface IPagedChangeSet : ISortedChangeSet 16 | where TObject : notnull 17 | where TKey : notnull 18 | { 19 | /// 20 | /// Gets the parameters used to virtualise the stream. 21 | /// 22 | IPageResponse Response { get; } 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IQuery.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// Exposes internal cache state to enable querying. 10 | /// 11 | /// The type of the object. 12 | /// The type of the key. 13 | public interface IQuery 14 | where TObject : notnull 15 | { 16 | /// 17 | /// Gets the count. 18 | /// 19 | int Count { get; } 20 | 21 | /// 22 | /// Gets the items. 23 | /// 24 | IEnumerable Items { get; } 25 | 26 | /// 27 | /// Gets the keys. 28 | /// 29 | IEnumerable Keys { get; } 30 | 31 | /// 32 | /// Gets the items together with their keys. 33 | /// 34 | /// 35 | /// The key values. 36 | /// 37 | IEnumerable> KeyValues { get; } 38 | 39 | /// 40 | /// Lookup a single item using the specified key. 41 | /// 42 | /// 43 | /// Fast indexed lookup. 44 | /// 45 | /// The key. 46 | /// The looked up value. 47 | Optional Lookup(TKey key); 48 | } 49 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/ISortedChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// An update collection as per the system convention additionally providing a sorted set of the underling state. 9 | /// 10 | /// The type of the object. 11 | /// The type of the key. 12 | public interface ISortedChangeSet : IChangeSet 13 | where TObject : notnull 14 | where TKey : notnull 15 | { 16 | /// 17 | /// Gets all cached items in sort order. 18 | /// 19 | IKeyValueCollection SortedItems { get; } 20 | } 21 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/ISortedChangeSetAdaptor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// A simple adaptor to inject side effects into a sorted change set observable. 9 | /// 10 | /// The type of the object. 11 | /// The type of the key. 12 | public interface ISortedChangeSetAdaptor 13 | where TObject : notnull 14 | where TKey : notnull 15 | { 16 | /// 17 | /// Adapts the specified change. 18 | /// 19 | /// The change. 20 | void Adapt(ISortedChangeSet changes); 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/ISourceCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// An observable cache which exposes an update API. Used at the root 10 | /// of all observable chains. 11 | /// 12 | /// The type of the object. 13 | /// The type of the key. 14 | public interface ISourceCache : IObservableCache 15 | where TObject : notnull 16 | where TKey : notnull 17 | { 18 | /// 19 | /// Gets key selector used by the cache to retrieve keys from objects. 20 | /// 21 | Func KeySelector { get; } 22 | 23 | /// 24 | /// Action to apply a batch update to a cache. Multiple update methods can be invoked within a single batch operation. 25 | /// These operations are invoked within the cache's lock and is therefore thread safe. 26 | /// The result of the action will produce a single change set. 27 | /// 28 | /// The update action. 29 | void Edit(Action> updateAction); 30 | } 31 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IVirtualChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Represents a subset of data reduced by a defined set of parameters. 9 | /// 10 | /// The type of the object. 11 | /// The type of the key. 12 | public interface IVirtualChangeSet : ISortedChangeSet 13 | where TObject : notnull 14 | where TKey : notnull 15 | { 16 | /// 17 | /// Gets the parameters used to virtualise the stream. 18 | /// 19 | IVirtualResponse Response { get; } 20 | } 21 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IVirtualRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// A request to virtualise a stream. 9 | /// 10 | public interface IVirtualRequest 11 | { 12 | /// 13 | /// Gets the number of records to return. 14 | /// 15 | int Size { get; } 16 | 17 | /// 18 | /// Gets the start index. 19 | /// 20 | int StartIndex { get; } 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/IVirtualResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Defines values used to virtualise the result set. 9 | /// 10 | public interface IVirtualResponse 11 | { 12 | /// 13 | /// Gets the requested size of the virtualised data. 14 | /// 15 | int Size { get; } 16 | 17 | /// 18 | /// Gets the start index. 19 | /// 20 | int StartIndex { get; } 21 | 22 | /// 23 | /// Gets the total size of the underlying cache. 24 | /// 25 | /// 26 | /// The total size. 27 | /// 28 | int TotalSize { get; } 29 | } 30 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/AnonymousQuery.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal sealed class AnonymousQuery(Cache cache) : IQuery 8 | where TObject : notnull 9 | where TKey : notnull 10 | { 11 | private readonly Cache _cache = cache.Clone(); 12 | 13 | public int Count => _cache.Count; 14 | 15 | public IEnumerable Items => _cache.Items; 16 | 17 | public IEnumerable Keys => _cache.Keys; 18 | 19 | public IEnumerable> KeyValues => _cache.KeyValues; 20 | 21 | public Optional Lookup(TKey key) => _cache.Lookup(key); 22 | } 23 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/CacheEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal static class CacheEx 8 | { 9 | public static void Clone(this IDictionary source, IChangeSet changes) 10 | where TKey : notnull 11 | where TObject : notnull 12 | { 13 | foreach (var item in changes.ToConcreteType()) 14 | { 15 | switch (item.Reason) 16 | { 17 | case ChangeReason.Update: 18 | case ChangeReason.Add: 19 | source[item.Key] = item.Current; 20 | break; 21 | 22 | case ChangeReason.Remove: 23 | source.Remove(item.Key); 24 | break; 25 | } 26 | } 27 | } 28 | 29 | public static IChangeSet GetInitialUpdates(this ChangeAwareCache source, Func? filter = null) 30 | where TObject : notnull 31 | where TKey : notnull 32 | { 33 | var filtered = filter is null ? source.KeyValues : source.KeyValues.Where(kv => filter(kv.Value)); 34 | return new ChangeSet(filtered.Select(i => new Change(ChangeReason.Add, i.Key, i.Value))); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/Cast.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.Cache.Internal; 8 | 9 | internal sealed class Cast(IObservable> source, Func converter) 10 | where TSource : notnull 11 | where TKey : notnull 12 | where TDestination : notnull 13 | { 14 | private readonly Func _converter = converter ?? throw new ArgumentNullException(nameof(converter)); 15 | 16 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 17 | 18 | public IObservable> Run() => _source.Select( 19 | changes => 20 | { 21 | var transformed = changes.ToConcreteType().Select(change => new Change(change.Reason, change.Key, _converter(change.Current), change.Previous.Convert(_converter), change.CurrentIndex, change.PreviousIndex)); 22 | return new ChangeSet(transformed); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/ChangeSetCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.Cache.Internal; 8 | 9 | /// 10 | /// Wraps an Observable ChangeSet while maintaining a copy of the aggregated changes. 11 | /// 12 | /// ChangeSet Object Type. 13 | /// ChangeSet Key Type. 14 | internal sealed class ChangeSetCache 15 | where TObject : notnull 16 | where TKey : notnull 17 | { 18 | public ChangeSetCache(IObservable> source) => 19 | Source = source.Do(Cache.Clone); 20 | 21 | public Cache Cache { get; } = new(); 22 | 23 | public IObservable> Source { get; } 24 | } 25 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/CombineOperator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | /// 8 | /// How the multiple streams are combinedL. 9 | /// 10 | public enum CombineOperator 11 | { 12 | /// 13 | /// Apply a logical And between two or more observable change sets. 14 | /// 15 | And, 16 | 17 | /// 18 | /// Apply a logical Or between two or more observable change sets. 19 | /// 20 | Or, 21 | 22 | /// 23 | /// Apply a logical Xor between two or more observable change sets. 24 | /// 25 | Xor, 26 | 27 | /// 28 | /// Include the items in the first change set and exclude any items belonging to the other. 29 | /// 30 | Except 31 | } 32 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/DeferUntilLoaded.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.Cache.Internal; 8 | 9 | internal sealed class DeferUntilLoaded 10 | where TObject : notnull 11 | where TKey : notnull 12 | { 13 | private readonly IObservable> _result; 14 | 15 | public DeferUntilLoaded(IObservableCache source) 16 | { 17 | source.ThrowArgumentNullExceptionIfNull(nameof(source)); 18 | 19 | _result = source.CountChanged.Where(count => count != 0).Take(1).Select(_ => new ChangeSet()).Concat(source.Connect()).NotEmpty(); 20 | } 21 | 22 | public DeferUntilLoaded(IObservable> source) => _result = source.MonitorStatus().Where(status => status == ConnectionStatus.Loaded).Take(1).Select(_ => new ChangeSet()).Concat(source).NotEmpty(); 23 | 24 | public IObservable> Run() => _result; 25 | } 26 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal static class DictionaryExtensions 8 | { 9 | internal static IEnumerable GetOrEmpty(this IDictionary> dict, TDictKey key) => 10 | dict.TryGetValue(key, out var value) ? value : Enumerable.Empty(); 11 | } 12 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/EditDiffChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal sealed class EditDiffChangeSet(IObservable> source, Func keySelector, IEqualityComparer? equalityComparer) 8 | where TObject : notnull 9 | where TKey : notnull 10 | { 11 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 12 | 13 | private readonly IEqualityComparer _equalityComparer = equalityComparer ?? EqualityComparer.Default; 14 | 15 | private readonly Func _keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector)); 16 | 17 | public IObservable> Run() => 18 | ObservableChangeSet.Create( 19 | cache => _source.Subscribe(items => cache.EditDiff(items, _equalityComparer), () => cache.Dispose()), 20 | _keySelector); 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/ExpirableItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal readonly struct ExpirableItem(TObject value, TKey key, DateTime dateTime, long index = 0) : IEquatable> 8 | { 9 | public TObject Value { get; } = value; 10 | 11 | public TKey Key { get; } = key; 12 | 13 | public DateTime ExpireAt { get; } = dateTime; 14 | 15 | public long Index { get; } = index; 16 | 17 | public static bool operator ==(in ExpirableItem left, in ExpirableItem right) => left.Equals(right); 18 | 19 | public static bool operator !=(in ExpirableItem left, in ExpirableItem right) => !left.Equals(right); 20 | 21 | /// 22 | public bool Equals(ExpirableItem other) => EqualityComparer.Default.Equals(Key, other.Key) && ExpireAt.Equals(other.ExpireAt); 23 | 24 | /// 25 | public override bool Equals(object? obj) => obj is ExpirableItem expItem && Equals(expItem); 26 | 27 | /// 28 | public override int GetHashCode() 29 | { 30 | unchecked 31 | { 32 | return (Key is null ? 0 : EqualityComparer.Default.GetHashCode(Key) * 397) ^ ExpireAt.GetHashCode(); 33 | } 34 | } 35 | 36 | public override string ToString() => $"Key: {Key}, Expire At: {ExpireAt}"; 37 | } 38 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/FilterOnObservable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Concurrency; 6 | using System.Reactive.Linq; 7 | 8 | namespace DynamicData.Cache.Internal; 9 | 10 | internal sealed class FilterOnObservable(IObservable> source, Func> filterFactory, TimeSpan? buffer = null, IScheduler? scheduler = null) 11 | where TObject : notnull 12 | where TKey : notnull 13 | { 14 | private readonly Func> _filterFactory = filterFactory ?? throw new ArgumentNullException(nameof(filterFactory)); 15 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 16 | 17 | public IObservable> Run() => 18 | _source 19 | .Transform((val, key) => new FilterProxy(val, _filterFactory(val, key))) 20 | .AutoRefreshOnObservable(proxy => proxy.FilterObservable, buffer, scheduler) 21 | .Filter(proxy => proxy.PassesFilter) 22 | .TransformImmutable(proxy => proxy.Value); 23 | 24 | private sealed class FilterProxy(TObject obj, IObservable observable) 25 | { 26 | public TObject Value { get; } = obj; 27 | 28 | public bool PassesFilter { get; private set; } 29 | 30 | public IObservable FilterObservable => observable.DistinctUntilChanged().Do(filterValue => PassesFilter = filterValue); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/FilterOnProperty.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | using System.Linq.Expressions; 7 | using System.Reactive.Concurrency; 8 | 9 | namespace DynamicData.Cache.Internal; 10 | 11 | [Obsolete("Use AutoRefresh(), followed by Filter() instead")] 12 | internal sealed class FilterOnProperty(IObservable> source, Expression> propertySelector, Func predicate, TimeSpan? throttle = null, IScheduler? scheduler = null) 13 | where TObject : INotifyPropertyChanged 14 | where TKey : notnull 15 | { 16 | public IObservable> Run() => source.AutoRefresh(propertySelector, propertyChangeThrottle: throttle, scheduler: scheduler).Filter(predicate); 17 | } 18 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/GroupOnProperty.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | using System.Linq.Expressions; 7 | using System.Reactive.Concurrency; 8 | using System.Reactive.Linq; 9 | 10 | namespace DynamicData.Cache.Internal; 11 | 12 | internal sealed class GroupOnProperty(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler? scheduler = null) 13 | where TObject : INotifyPropertyChanged 14 | where TKey : notnull 15 | where TGroup : notnull 16 | { 17 | private readonly Func _groupSelector = groupSelectorKey.Compile(); 18 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 19 | 20 | public IObservable> Run() => _source.Publish( 21 | shared => 22 | { 23 | // Monitor explicit property changes 24 | var regrouper = shared.WhenValueChanged(groupSelectorKey, false).ToUnit(); 25 | 26 | // add a throttle if specified 27 | if (throttle is not null) 28 | { 29 | regrouper = regrouper.Throttle(throttle.Value, scheduler ?? GlobalConfig.DefaultScheduler); 30 | } 31 | 32 | // Use property changes as a trigger to re-evaluate Grouping 33 | return shared.Group(_groupSelector, regrouper); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/IFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | /// 8 | /// Provides a filter. 9 | /// 10 | /// The type of the object. 11 | /// The type of the field. 12 | internal interface IFilter 13 | where TObject : notnull 14 | where TKey : notnull 15 | { 16 | /// 17 | /// Gets the filter to use. 18 | /// 19 | Func Filter { get; } 20 | 21 | /// 22 | /// Provides a change set with refreshed items. 23 | /// 24 | /// The items to refresh. 25 | /// A change set of the changes. 26 | IChangeSet Refresh(IEnumerable> items); 27 | 28 | /// 29 | /// Provides a change set with updated items. 30 | /// 31 | /// The items to update. 32 | /// A change set of the changes. 33 | IChangeSet Update(IChangeSet updates); 34 | } 35 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/IKeySelector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | /// 8 | /// Selects a key from a item. 9 | /// 10 | /// The type of the object. 11 | /// The type of the key. 12 | internal interface IKeySelector // : IKeySelector 13 | { 14 | /// 15 | /// Gets the key from the object. 16 | /// 17 | /// The item to get the key for. 18 | /// The key. 19 | TKey GetKey(TObject item); 20 | } 21 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/ImmutableGroupChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal sealed class ImmutableGroupChangeSet : ChangeSet, TGroupKey>, IImmutableGroupChangeSet 8 | where TObject : notnull 9 | where TKey : notnull 10 | where TGroupKey : notnull 11 | { 12 | public static new readonly IImmutableGroupChangeSet Empty = new ImmutableGroupChangeSet(); 13 | 14 | public ImmutableGroupChangeSet(IEnumerable, TGroupKey>> items) 15 | : base(items) 16 | { 17 | } 18 | 19 | private ImmutableGroupChangeSet() 20 | { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/IndexAndNode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace DynamicData.Cache.Internal; 8 | 9 | internal static class IndexAndNode 10 | { 11 | public static IndexAndNode Create(int index, LinkedListNode value) => new(index, value); 12 | } 13 | 14 | [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Same class name, different generics.")] 15 | internal sealed class IndexAndNode(int index, LinkedListNode node) 16 | { 17 | public int Index { get; } = index; 18 | 19 | public LinkedListNode Node { get; } = node; 20 | } 21 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/InnerJoinMany.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal sealed class InnerJoinMany(IObservable> left, IObservable> right, Func rightKeySelector, Func, TDestination> resultSelector) 8 | where TLeft : notnull 9 | where TLeftKey : notnull 10 | where TRight : notnull 11 | where TRightKey : notnull 12 | where TDestination : notnull 13 | { 14 | private readonly IObservable> _left = left ?? throw new ArgumentNullException(nameof(left)); 15 | 16 | private readonly Func, TDestination> _resultSelector = resultSelector ?? throw new ArgumentNullException(nameof(resultSelector)); 17 | 18 | private readonly IObservable> _right = right ?? throw new ArgumentNullException(nameof(right)); 19 | 20 | private readonly Func _rightKeySelector = rightKeySelector ?? throw new ArgumentNullException(nameof(rightKeySelector)); 21 | 22 | public IObservable> Run() 23 | { 24 | var rightGrouped = _right.GroupWithImmutableState(_rightKeySelector); 25 | return _left.InnerJoin(rightGrouped, grouping => grouping.Key, (key, left, right) => _resultSelector(key.leftKey, left, right)).ChangeKey((keyTuple, _) => keyTuple.leftKey); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/KeyComparer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal sealed class KeyComparer : IEqualityComparer> 8 | { 9 | public bool Equals(KeyValuePair x, KeyValuePair y) => x.Key?.Equals(y.Key) ?? false; 10 | 11 | public int GetHashCode(KeyValuePair obj) => obj.Key is null ? 0 : obj.Key.GetHashCode(); 12 | } 13 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/KeySelector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace DynamicData.Cache.Internal; 8 | 9 | internal sealed class KeySelector(Func keySelector) : IKeySelector 10 | { 11 | private readonly Func _keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector)); 12 | 13 | [SuppressMessage("Design", "CA1822: Member can be static", Justification = "Backwards compatibilty")] 14 | public Type Type => typeof(TObject); 15 | 16 | public TKey GetKey(TObject item) 17 | { 18 | try 19 | { 20 | return _keySelector(item); 21 | } 22 | catch (Exception ex) 23 | { 24 | throw new KeySelectorException("Error returning key", ex); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/KeySelectorException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | /// 8 | /// An exception that happens when there is a problem with the key selector. 9 | /// 10 | [Serializable] 11 | public class KeySelectorException : Exception 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | public KeySelectorException() 17 | { 18 | } 19 | 20 | /// 21 | /// Initializes a new instance of the class with a specified error message. 22 | /// 23 | /// The message that describes the error. 24 | public KeySelectorException(string message) 25 | : base(message) 26 | { 27 | } 28 | 29 | /// 30 | /// 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. 31 | /// 32 | /// 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. 33 | public KeySelectorException(string message, Exception innerException) 34 | : base(message, innerException) 35 | { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/KeyValueComparer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal sealed class KeyValueComparer(IComparer? comparer = null) : IComparer> 8 | { 9 | public int Compare(KeyValuePair x, KeyValuePair y) 10 | { 11 | if (comparer is not null) 12 | { 13 | var result = comparer.Compare(x.Value, y.Value); 14 | 15 | if (result != 0) 16 | { 17 | return result; 18 | } 19 | } 20 | 21 | if (x.Key is null && y.Key is null) 22 | { 23 | return 0; 24 | } 25 | 26 | if (x.Key is null) 27 | { 28 | return 1; 29 | } 30 | 31 | if (y.Key is null) 32 | { 33 | return -1; 34 | } 35 | 36 | if (x.Key is IComparable xComp) 37 | { 38 | return xComp.CompareTo(y.Key); 39 | } 40 | 41 | return x.Key.GetHashCode().CompareTo(y.Key.GetHashCode()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/ObservableWithValue.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.Cache.Internal; 8 | 9 | internal sealed class ObservableWithValue 10 | where TValue : notnull 11 | { 12 | public ObservableWithValue(TObject item, IObservable source) 13 | { 14 | Item = item; 15 | Observable = source.Do(value => LatestValue = value); 16 | } 17 | 18 | public TObject Item { get; } 19 | 20 | public Optional LatestValue { get; private set; } = Optional.None; 21 | 22 | public IObservable Observable { get; } 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/Internal/RightJoinMany.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Cache.Internal; 6 | 7 | internal sealed class RightJoinMany(IObservable> left, IObservable> right, Func rightKeySelector, Func, IGrouping, TDestination> resultSelector) 8 | where TLeft : notnull 9 | where TLeftKey : notnull 10 | where TRight : notnull 11 | where TRightKey : notnull 12 | where TDestination : notnull 13 | { 14 | private readonly IObservable> _left = left ?? throw new ArgumentNullException(nameof(left)); 15 | 16 | private readonly Func, IGrouping, TDestination> _resultSelector = resultSelector ?? throw new ArgumentNullException(nameof(resultSelector)); 17 | 18 | private readonly IObservable> _right = right ?? throw new ArgumentNullException(nameof(right)); 19 | 20 | private readonly Func _rightKeySelector = rightKeySelector ?? throw new ArgumentNullException(nameof(rightKeySelector)); 21 | 22 | public IObservable> Run() 23 | { 24 | var rightGrouped = _right.GroupWithImmutableState(_rightKeySelector); 25 | return _left.RightJoin(rightGrouped, grouping => grouping.Key, (a, b, c) => _resultSelector(a, b, c)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/MissingKeyException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// Thrown when a key is expected in a cache but not found. 10 | /// 11 | [Serializable] 12 | public class MissingKeyException : Exception 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The message that describes the error. 18 | public MissingKeyException(string message) 19 | : base(message) 20 | { 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | public MissingKeyException() 27 | { 28 | } 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// The message that describes the error. 34 | /// A inner exception with further information. 35 | public MissingKeyException(string message, Exception innerException) 36 | : base(message, innerException) 37 | { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/PageContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using DynamicData.Operators; 6 | 7 | namespace DynamicData; 8 | 9 | /// 10 | /// Parameters associated with the page operation. 11 | /// 12 | /// The type of object. 13 | /// Response parameters. 14 | /// The comparer used to order the items. 15 | /// The options used to perform virtualization. 16 | public record PageContext( 17 | IPageResponse Response, 18 | IComparer Comparer, 19 | SortAndPageOptions Options) 20 | { 21 | internal static readonly PageContext Empty = new 22 | ( 23 | new PageResponse(0, 0, 0, 0), 24 | Comparer.Default, 25 | new SortAndPageOptions() 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/SortAndPageOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using DynamicData.Binding; 6 | 7 | namespace DynamicData; 8 | 9 | /// 10 | /// Options for the sort and virtualize operator. 11 | /// 12 | public record struct SortAndPageOptions() 13 | { 14 | /// 15 | /// The sort reset threshold ie the number of changes before a reset is fired. 16 | /// 17 | public int ResetThreshold { get; init; } = BindingOptions.DefaultResetThreshold; 18 | 19 | /// 20 | /// Use binary search when the result of the comparer is a pure function. 21 | /// 22 | public bool UseBinarySearch { get; init; } 23 | 24 | /// 25 | /// Set the initial capacity of internal sorted list. 26 | /// 27 | public int InitialCapacity { get; init; } 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/SortAndVirtualizeOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using DynamicData.Binding; 6 | 7 | namespace DynamicData; 8 | 9 | /// 10 | /// Options for the sort and virtualize operator. 11 | /// 12 | public record struct SortAndVirtualizeOptions() 13 | { 14 | /// 15 | /// The sort reset threshold ie the number of changes before a reset is fired. 16 | /// 17 | public int ResetThreshold { get; init; } = BindingOptions.DefaultResetThreshold; 18 | 19 | /// 20 | /// Use binary search when the result of the comparer is a pure function. 21 | /// 22 | public bool UseBinarySearch { get; init; } 23 | 24 | /// 25 | /// Set the initial capacity of internal sorted list. 26 | /// 27 | public int InitialCapacity { get; init; } 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/SortOptimisations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// Flags used to specify one or more sort optimisations. 10 | /// 11 | [Flags] 12 | public enum SortOptimisations 13 | { 14 | /// 15 | /// No sorting optimisation are applied. 16 | /// 17 | None = 0, 18 | 19 | /// 20 | /// Specify this option if the comparer used for sorting compares immutable fields only. 21 | /// In which case index changes can be calculated using BinarySearch rather than the expensive IndexOf. 22 | /// 23 | ComparesImmutableValuesOnly = 1, 24 | 25 | /// 26 | /// Ignores moves because of evaluates. 27 | /// Use for virtualisatiom or pagination. 28 | /// 29 | IgnoreEvaluates = 2, 30 | 31 | /// 32 | /// The insert at end then sort entire set. This can be the best algorithm for large data sets with many changes. 33 | /// 34 | [Obsolete("This is no longer being used. Use one of the other options instead.")] 35 | InsertAtEndThenSort = 3 36 | } 37 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/SortReason.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// The reason why the sorted collection has changed. 9 | /// 10 | public enum SortReason 11 | { 12 | /// 13 | /// The collection has loaded for the first time. 14 | /// 15 | InitialLoad, 16 | 17 | /// 18 | /// The comparer used to sort has changed. 19 | /// 20 | ComparerChanged, 21 | 22 | /// 23 | /// The data changed. 24 | /// 25 | DataChanged, 26 | 27 | /// 28 | /// Sorting has been reapplied. 29 | /// 30 | Reorder, 31 | 32 | /// 33 | /// A large number of changes has been received and the reset threshold has been exceeded. 34 | /// The entire set has been resorted without moves being calculated. 35 | /// 36 | Reset 37 | } 38 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/SortedChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using DynamicData.Cache.Internal; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace DynamicData; 9 | 10 | internal sealed class SortedChangeSet : ChangeSet, ISortedChangeSet 11 | where TObject : notnull 12 | where TKey : notnull 13 | { 14 | public static new readonly ISortedChangeSet Empty = new SortedChangeSet(); 15 | 16 | public SortedChangeSet(IKeyValueCollection sortedItems, IEnumerable> updates) 17 | : base(updates) => SortedItems = sortedItems; 18 | 19 | private SortedChangeSet() => SortedItems = new KeyValueCollection(); 20 | 21 | public IKeyValueCollection SortedItems { get; } 22 | 23 | public bool Equals(SortedChangeSet other) => SortedItems.SequenceEqual(other.SortedItems); 24 | 25 | public override bool Equals(object? obj) => obj is SortedChangeSet value && Equals(value); 26 | 27 | public override int GetHashCode() => SortedItems.GetHashCode(); 28 | 29 | public override string ToString() => $"SortedChangeSet. Count= {SortedItems.Count}. Updates = {Count}"; 30 | } 31 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/SourceCacheEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// Source cache convenience extensions. 10 | /// 11 | public static class SourceCacheEx 12 | { 13 | /// 14 | /// Connects to the cache, and casts the object to the specified type 15 | /// Alas, I had to add the converter due to type inference issues. 16 | /// 17 | /// The type of the object. 18 | /// The type of the key. 19 | /// The type of the destination. 20 | /// The source. 21 | /// The conversion factory. 22 | /// An observable which emits the change set. 23 | public static IObservable> Cast(this IObservableCache source, Func converter) 24 | where TSource : notnull 25 | where TKey : notnull 26 | where TDestination : notnull => source is null ? throw new ArgumentNullException(nameof(source)) : source.Connect().Cast(converter); 27 | } 28 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/TransformAsyncOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | namespace DynamicData; 5 | 6 | /// 7 | /// Options for TransformAsync and TransformSafeAsync. 8 | /// 9 | /// The maximum number of tasks in flight at once. 10 | /// Should a new transform be applied when a refresh event is received. 11 | public record struct TransformAsyncOptions(int? MaximumConcurrency, bool TransformOnRefresh) 12 | { 13 | /// 14 | /// The default transform async option values, with is unlimited concurrency and do not transform on reset. 15 | /// 16 | /// A TransformAsyncOptions object. 17 | public static readonly TransformAsyncOptions Default = new(null, false); 18 | } 19 | -------------------------------------------------------------------------------- /src/DynamicData/Cache/VirtualContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Parameters associated with the virtualize operation. 9 | /// 10 | /// The type of object. 11 | /// Response parameters. 12 | /// The comparer used to order the items. 13 | /// The options used to perform virtualization. 14 | public record VirtualContext( 15 | IVirtualResponse Response, 16 | IComparer Comparer, 17 | SortAndVirtualizeOptions Options) 18 | { 19 | internal static readonly VirtualContext Empty = new 20 | ( 21 | new VirtualResponse(0, 0, 0), 22 | Comparer.Default, 23 | new SortAndVirtualizeOptions() 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/DynamicData/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | internal static class Constants 8 | { 9 | public const string VirtualizeIsObsolete = "Use SortAndVirtualize as it's more efficient"; 10 | public const string PageIsObsolete = "Use SortAndPage as it's more efficient"; 11 | public const string TopIsObsolete = "Use Overload with comparer as it's more efficient"; 12 | public const string SortIsObsolete = "Use SortAndBind as it's more efficient"; 13 | } 14 | -------------------------------------------------------------------------------- /src/DynamicData/DynamicData.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;net462;net6.0;net7.0;net8.0;net9.0 4 | true 5 | true 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Dynamic Data 19 | 20 | Bring the power of Rx to collections using Dynamic Data. 21 | Dynamic Data is a comprehensive caching and data manipulation solution which introduces domain centric observable collections. 22 | Linq extensions enable dynamic filtering, sorting, grouping, transforms, binding, pagination, data virtualisation, expiration, disposal management plus more. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ObservableCacheEx.cs 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/DynamicData/DynamicDataOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using DynamicData.Binding; 6 | 7 | namespace DynamicData; 8 | 9 | /// 10 | /// System-wide options container. 11 | /// 12 | public static class DynamicDataOptions 13 | { 14 | /// 15 | /// Gets or sets the system-wide default values for all Bind operations. 16 | /// 17 | public static BindingOptions Binding { get; set; } = new(BindingOptions.DefaultResetThreshold); 18 | 19 | /// 20 | /// Gets or sets the system-wide default values for all SortAndBind operations. 21 | /// 22 | public static SortAndBindOptions SortAndBind { get; set; } = new(); 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/Experimental/ExperimentalEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Concurrency; 6 | 7 | namespace DynamicData.Experimental; 8 | 9 | /// 10 | /// Experimental operator extensions. 11 | /// 12 | public static class ExperimentalEx 13 | { 14 | /// 15 | /// Wraps the source cache, optimising it for watching individual updates. 16 | /// 17 | /// The type of the object. 18 | /// The type of the key. 19 | /// The source. 20 | /// The scheduler. 21 | /// The watcher. 22 | /// source. 23 | public static IWatcher AsWatcher(this IObservable> source, IScheduler? scheduler = null) 24 | where TObject : notnull 25 | where TKey : notnull 26 | { 27 | source.ThrowArgumentNullExceptionIfNull(nameof(source)); 28 | 29 | return new Watcher(source, scheduler ?? GlobalConfig.DefaultScheduler); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DynamicData/Experimental/ISubjectWithRefCount.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Subjects; 6 | 7 | namespace DynamicData.Experimental; 8 | 9 | /// 10 | /// A subject which also contains its current reference count. 11 | /// 12 | /// The type of item. 13 | internal interface ISubjectWithRefCount : ISubject 14 | { 15 | /// Gets number of subscribers. 16 | /// 17 | /// The ref count. 18 | /// 19 | int RefCount { get; } 20 | } 21 | -------------------------------------------------------------------------------- /src/DynamicData/Experimental/IWatcher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Experimental; 6 | 7 | /// 8 | /// A specialisation of the SourceList which is optimised for watching individual items. 9 | /// 10 | /// The type of the object. 11 | /// The type of the key. 12 | public interface IWatcher : IDisposable 13 | where TObject : notnull 14 | where TKey : notnull 15 | { 16 | /// 17 | /// Watches updates which match the specified key. 18 | /// 19 | /// The key. 20 | /// An observable which emits the change. 21 | IObservable> Watch(TKey key); 22 | } 23 | -------------------------------------------------------------------------------- /src/DynamicData/GlobalConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Concurrency; 6 | 7 | namespace DynamicData; 8 | 9 | internal static class GlobalConfig 10 | { 11 | public static IScheduler DefaultScheduler => TaskPoolScheduler.Default; 12 | } 13 | -------------------------------------------------------------------------------- /src/DynamicData/IChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Base interface representing a set of changes. 9 | /// 10 | public interface IChangeSet 11 | { 12 | /// 13 | /// Gets the number of additions. 14 | /// 15 | int Adds { get; } 16 | 17 | /// 18 | /// Gets or sets the capacity of the change set. 19 | /// 20 | /// 21 | /// The capacity. 22 | /// 23 | int Capacity { get; set; } 24 | 25 | /// 26 | /// Gets the total update count. 27 | /// 28 | int Count { get; } 29 | 30 | /// 31 | /// Gets the number of moves. 32 | /// 33 | int Moves { get; } 34 | 35 | /// 36 | /// Gets the number of refreshes. 37 | /// 38 | int Refreshes { get; } 39 | 40 | /// 41 | /// Gets the number of removes. 42 | /// 43 | int Removes { get; } 44 | } 45 | -------------------------------------------------------------------------------- /src/DynamicData/Internal/ExceptionMixins.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData 6 | { 7 | internal static class ExceptionMixins 8 | { 9 | public static void ThrowArgumentNullExceptionIfNull(this T? value, string name) 10 | { 11 | if (value is null) 12 | { 13 | throw new ArgumentNullException(name); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/DynamicData/Internal/ObservableEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive; 6 | 7 | namespace DynamicData.Internal; 8 | 9 | internal static class ObservableEx 10 | { 11 | public static IDisposable SubscribeSafe(this IObservable observable, Action onNext, Action onError, Action onCompleted) => 12 | observable.SubscribeSafe(Observer.Create(onNext, onError, onCompleted)); 13 | 14 | public static IDisposable SubscribeSafe(this IObservable observable, Action onNext, Action onError) => 15 | observable.SubscribeSafe(Observer.Create(onNext, onError)); 16 | 17 | public static IDisposable SubscribeSafe(this IObservable observable, Action onError, Action onCompleted) => 18 | observable.SubscribeSafe(Observer.Create(Stub.Ignore, onError, onCompleted)); 19 | 20 | public static IDisposable SubscribeSafe(this IObservable observable, Action onError) => 21 | observable.SubscribeSafe(Observer.Create(Stub.Ignore, onError)); 22 | 23 | private static class Stub 24 | { 25 | public static readonly Action Ignore = static _ => { }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/DynamicData/Internal/Rxx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | #if NET9_0_OR_GREATER 6 | using DynamicData.Internal; 7 | 8 | namespace System.Reactive.Linq; 9 | 10 | internal static class Rxx 11 | { 12 | /// 13 | /// Keep this class internal as it should be supplied by System.Reactive and probably will be one day. 14 | /// 15 | public static IObservable Synchronize(this IObservable source, Lock locker) 16 | { 17 | return Observable.Create(observer => 18 | { 19 | return source.SubscribeSafe(t => 20 | { 21 | lock (locker) 22 | { 23 | observer.OnNext(t); 24 | } 25 | }, ex => 26 | { 27 | lock (locker) 28 | { 29 | observer.OnError(ex); 30 | } 31 | }, () => 32 | { 33 | lock (locker) 34 | { 35 | observer.OnCompleted(); 36 | } 37 | }); 38 | }); 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /src/DynamicData/Internal/SwappableLock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | internal ref struct SwappableLock 8 | { 9 | public static SwappableLock CreateAndEnter(object gate) 10 | { 11 | var result = new SwappableLock() 12 | { 13 | _gate = gate 14 | }; 15 | 16 | Monitor.Enter(gate, ref result._hasLock); 17 | 18 | return result; 19 | } 20 | 21 | public void SwapTo(object gate) 22 | { 23 | if (_gate is null) 24 | throw new InvalidOperationException("Lock is not initialized"); 25 | 26 | var hasNewLock = false; 27 | Monitor.Enter(gate, ref hasNewLock); 28 | 29 | if (_hasLock) 30 | Monitor.Exit(_gate); 31 | 32 | _hasLock = hasNewLock; 33 | _gate = gate; 34 | } 35 | 36 | public void Dispose() 37 | { 38 | if (_hasLock && (_gate is not null)) 39 | { 40 | Monitor.Exit(_gate); 41 | _hasLock = false; 42 | _gate = null; 43 | } 44 | } 45 | 46 | private bool _hasLock; 47 | private object? _gate; 48 | } 49 | -------------------------------------------------------------------------------- /src/DynamicData/Kernel/ConnectionStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Kernel; 6 | 7 | /// 8 | /// Connectable cache status. 9 | /// 10 | public enum ConnectionStatus 11 | { 12 | /// 13 | /// Status set to pending until first batch of data is received. 14 | /// 15 | Pending = 0, 16 | 17 | /// 18 | /// Status set to loaded when first batch of data has been received. Remains loaded 19 | /// until the cache is disposed or faults. 20 | /// 21 | Loaded = 1, 22 | 23 | /// 24 | /// There has been a error and the stream has stopped. No more status updates will be received. 25 | /// 26 | Errored = 2, 27 | 28 | /// 29 | /// The stream has completed. No more status updates will be received. 30 | /// 31 | Completed = 3 32 | } 33 | -------------------------------------------------------------------------------- /src/DynamicData/Kernel/EnumeratorIList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections; 6 | 7 | // Lifted from here https://github.com/benaadams/Ben.Enumerable. Many thanks to the genius of the man. 8 | namespace DynamicData.Kernel; 9 | 10 | internal struct EnumeratorIList(IList list) : IEnumerator 11 | { 12 | private int _index = -1; 13 | 14 | public readonly T Current => list[_index]; 15 | 16 | readonly object? IEnumerator.Current => Current; 17 | 18 | public bool MoveNext() 19 | { 20 | _index++; 21 | 22 | return _index < list.Count; 23 | } 24 | 25 | public void Dispose() 26 | { 27 | } 28 | 29 | public void Reset() => _index = -1; 30 | } 31 | -------------------------------------------------------------------------------- /src/DynamicData/Kernel/IEnumerableIList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // Lifted from here https://github.com/benaadams/Ben.Enumerable. Many thanks to the genius of the man. 6 | namespace DynamicData.Kernel; 7 | 8 | /// 9 | /// A enumerable that also contains the enumerable list. 10 | /// 11 | /// The type of items. 12 | internal interface IEnumerableIList : IEnumerable 13 | { 14 | /// 15 | /// Gets the enumerator. 16 | /// 17 | /// The enumerator. 18 | new EnumeratorIList GetEnumerator(); 19 | } 20 | -------------------------------------------------------------------------------- /src/DynamicData/Kernel/ISupportsCapacity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Kernel; 6 | 7 | /// 8 | /// A collection type that supports a capacity. 9 | /// 10 | internal interface ISupportsCapacity 11 | { 12 | /// 13 | /// Gets or sets the capacity. 14 | /// 15 | int Capacity { get; set; } 16 | 17 | /// 18 | /// Gets the number of items. 19 | /// 20 | int Count { get; } 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/Kernel/OptionElse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Kernel; 6 | 7 | /// 8 | /// Continuation container used for the else operator on an option object. 9 | /// 10 | public sealed class OptionElse 11 | { 12 | internal static readonly OptionElse NoAction = new(false); 13 | 14 | private readonly bool _shouldRunAction; 15 | 16 | internal OptionElse(bool shouldRunAction = true) => _shouldRunAction = shouldRunAction; 17 | 18 | /// 19 | /// Invokes the specified action when an option has no value. 20 | /// 21 | /// The action. 22 | /// action. 23 | public void Else(Action action) 24 | { 25 | action.ThrowArgumentNullExceptionIfNull(nameof(action)); 26 | 27 | if (_shouldRunAction) 28 | { 29 | action(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DynamicData/Kernel/ParallelEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Kernel; 6 | 7 | internal static class ParallelEx 8 | { 9 | public static async Task> SelectParallel(this IEnumerable source, Func> selector, int maximumThreads = 5) 10 | { 11 | source.ThrowArgumentNullExceptionIfNull(nameof(source)); 12 | selector.ThrowArgumentNullExceptionIfNull(nameof(selector)); 13 | 14 | var semaphore = new SemaphoreSlim(maximumThreads); 15 | var tasks = new List>(); 16 | 17 | foreach (var item in source) 18 | { 19 | await semaphore.WaitAsync().ConfigureAwait(false); 20 | 21 | tasks.Add( 22 | Task.Run( 23 | async () => 24 | { 25 | try 26 | { 27 | return await selector(item).ConfigureAwait(false); 28 | } 29 | finally 30 | { 31 | semaphore.Release(); 32 | } 33 | })); 34 | } 35 | 36 | return await Task.WhenAll(tasks).ConfigureAwait(false); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DynamicData/Kernel/ReadOnlyCollectionLight.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections; 6 | 7 | namespace DynamicData.Kernel; 8 | 9 | internal sealed class ReadOnlyCollectionLight : IReadOnlyCollection 10 | { 11 | private readonly IList _items; 12 | 13 | public ReadOnlyCollectionLight(IEnumerable items) 14 | { 15 | _items = items.ToList(); 16 | Count = _items.Count; 17 | } 18 | 19 | private ReadOnlyCollectionLight() => _items = new List(); 20 | 21 | public static IReadOnlyCollection Empty { get; } = new ReadOnlyCollectionLight(); 22 | 23 | public int Count { get; } 24 | 25 | public IEnumerator GetEnumerator() => _items.GetEnumerator(); 26 | 27 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicData/Kernel/ReferenceEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.Kernel; 6 | 7 | internal sealed class ReferenceEqualityComparer : IEqualityComparer 8 | { 9 | public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); 10 | 11 | public bool Equals(T? x, T? y) => ReferenceEquals(x, y); 12 | 13 | public int GetHashCode(T? obj) => obj is null ? 0 : obj.GetHashCode(); 14 | } 15 | -------------------------------------------------------------------------------- /src/DynamicData/List/ChangeAwareListWithRefCounts.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using DynamicData.List.Internal; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace DynamicData; 9 | 10 | internal sealed class ChangeAwareListWithRefCounts : ChangeAwareList 11 | where T : notnull 12 | { 13 | private readonly ReferenceCountTracker _tracker = new(); 14 | 15 | public override void Clear() 16 | { 17 | _tracker.Clear(); 18 | base.Clear(); 19 | } 20 | 21 | public override bool Contains(T item) => _tracker.Contains(item); 22 | 23 | protected override void InsertItem(int index, T item) 24 | { 25 | _tracker.Add(item); 26 | base.InsertItem(index, item); 27 | } 28 | 29 | protected override void OnInsertItems(int startIndex, IEnumerable items) => items.ForEach(t => _tracker.Add(t)); 30 | 31 | protected override void OnRemoveItems(int startIndex, IEnumerable items) => items.ForEach(t => _tracker.Remove(t)); 32 | 33 | protected override void OnSetItem(int index, T newItem, T oldItem) 34 | { 35 | _tracker.Remove(oldItem); 36 | _tracker.Add(newItem); 37 | base.OnSetItem(index, newItem, oldItem); 38 | } 39 | 40 | protected override void RemoveItem(int index, T item) 41 | { 42 | _tracker.Remove(item); 43 | base.RemoveItem(index, item); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DynamicData/List/ChangeType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Description of the type of change. 9 | /// 10 | public enum ChangeType 11 | { 12 | /// 13 | /// A single item change. 14 | /// 15 | Item, 16 | 17 | /// 18 | /// A multiple item change. 19 | /// 20 | Range 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/List/IChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// A collection of changes. 10 | /// Changes are always published in the order. 11 | /// 12 | /// The type of the object. 13 | public interface IChangeSet : IEnumerable>, IChangeSet 14 | where TObject : notnull 15 | { 16 | /// 17 | /// Gets the number of updates. 18 | /// 19 | int Replaced { get; } 20 | 21 | /// 22 | /// Gets the total count of items changed. 23 | /// 24 | int TotalChanges { get; } 25 | } 26 | -------------------------------------------------------------------------------- /src/DynamicData/List/IChangeSetAdaptor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// A simple adaptor to inject side effects into a change set observable. 9 | /// 10 | /// The type of the object. 11 | public interface IChangeSetAdaptor 12 | where T : notnull 13 | { 14 | /// 15 | /// Adapts the specified change. 16 | /// 17 | /// The change. 18 | void Adapt(IChangeSet changes); 19 | } 20 | -------------------------------------------------------------------------------- /src/DynamicData/List/IGroup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// A grouping of observable lists. 9 | /// 10 | /// The type of the object. 11 | /// The type of the group. 12 | public interface IGroup 13 | where TObject : notnull 14 | { 15 | /// 16 | /// Gets the group key. 17 | /// 18 | TGroup GroupKey { get; } 19 | 20 | /// 21 | /// Gets the observable list. 22 | /// 23 | IObservableList List { get; } 24 | } 25 | -------------------------------------------------------------------------------- /src/DynamicData/List/IGrouping.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.List; 6 | 7 | /// 8 | /// Represents a group which provides an update after any value within the group changes. 9 | /// 10 | /// The type of the object. 11 | /// The type of the group key. 12 | public interface IGrouping 13 | { 14 | /// 15 | /// Gets the count. 16 | /// 17 | int Count { get; } 18 | 19 | /// 20 | /// Gets the items. 21 | /// 22 | IEnumerable Items { get; } 23 | 24 | /// 25 | /// Gets the group key. 26 | /// 27 | TGroupKey Key { get; } 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicData/List/IPageChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using DynamicData.Operators; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace DynamicData; 9 | 10 | /// 11 | /// Represents a paged subset of data reduced by a defined set of parameters 12 | /// Changes are always published in the order. 13 | /// 14 | /// The type of the object. 15 | public interface IPageChangeSet : IChangeSet 16 | where T : notnull 17 | { 18 | /// 19 | /// Gets the parameters used to virtualise the stream. 20 | /// 21 | IPageResponse Response { get; } 22 | } 23 | -------------------------------------------------------------------------------- /src/DynamicData/List/ISourceList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// An editable observable list, providing observable methods 10 | /// as well as data access methods. 11 | /// 12 | /// The type of the item. 13 | public interface ISourceList : IObservableList 14 | where T : notnull 15 | { 16 | /// 17 | /// Edit the inner list within the list's internal locking mechanism. 18 | /// 19 | /// The update action. 20 | void Edit(Action> updateAction); 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/List/IVirtualChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Represents a subset of data reduced by a defined set of parameters 9 | /// Changes are always published in the order. 10 | /// 11 | /// The type of the object. 12 | public interface IVirtualChangeSet : IChangeSet 13 | where T : notnull 14 | { 15 | /// 16 | /// Gets the parameters used to virtualise the stream. 17 | /// 18 | IVirtualResponse Response { get; } 19 | } 20 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/ClonedListChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.List.Internal; 8 | 9 | internal sealed class ClonedListChangeSet 10 | where TObject : notnull 11 | { 12 | public ClonedListChangeSet(IObservable> source, IEqualityComparer? equalityComparer) => 13 | Source = source.Do(changeSet => List.Clone(changeSet, equalityComparer)); 14 | 15 | public List List { get; } = []; 16 | 17 | public IObservable> Source { get; } 18 | } 19 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/DeferUntilLoaded.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.List.Internal; 8 | 9 | internal sealed class DeferUntilLoaded(IObservable> source) 10 | where T : notnull 11 | { 12 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 13 | 14 | public IObservable> Run() => _source.MonitorStatus().Where(status => status == ConnectionStatus.Loaded).Take(1).Select(_ => new ChangeSet()).Concat(_source).NotEmpty(); 15 | } 16 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/EditDiff.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData.List.Internal; 6 | 7 | internal sealed class EditDiff(ISourceList source, IEqualityComparer? equalityComparer) 8 | where T : notnull 9 | { 10 | private readonly IEqualityComparer _equalityComparer = equalityComparer ?? EqualityComparer.Default; 11 | 12 | private readonly ISourceList _source = source ?? throw new ArgumentNullException(nameof(source)); 13 | 14 | public void Edit(IEnumerable items) => _source.Edit( 15 | innerList => 16 | { 17 | var originalItems = innerList.AsArray(); 18 | var newItems = items.AsArray(); 19 | 20 | var removes = originalItems.Except(newItems, _equalityComparer); 21 | var adds = newItems.Except(originalItems, _equalityComparer); 22 | 23 | innerList.Remove(removes); 24 | innerList.AddRange(adds); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/FilterOnProperty.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | using System.Linq.Expressions; 7 | using System.Reactive.Concurrency; 8 | 9 | namespace DynamicData.List.Internal; 10 | 11 | [Obsolete("Use AutoRefresh(), followed by Filter() instead")] 12 | internal sealed class FilterOnProperty(IObservable> source, Expression> propertySelector, Func predicate, TimeSpan? throttle = null, IScheduler? scheduler = null) 13 | where TObject : INotifyPropertyChanged 14 | { 15 | public IObservable> Run() => source.AutoRefresh(propertySelector, propertyChangeThrottle: throttle, scheduler: scheduler).Filter(predicate); 16 | } 17 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/GroupOnProperty.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | using System.Linq.Expressions; 7 | using System.Reactive.Concurrency; 8 | using System.Reactive.Linq; 9 | 10 | namespace DynamicData.List.Internal; 11 | 12 | internal sealed class GroupOnProperty(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler? scheduler = null) 13 | where TObject : INotifyPropertyChanged 14 | where TGroup : notnull 15 | { 16 | private readonly Func _groupSelector = groupSelectorKey.Compile(); 17 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 18 | 19 | public IObservable>> Run() => _source.Publish( 20 | shared => 21 | { 22 | // Monitor explicit property changes 23 | var regrouper = shared.WhenValueChanged(groupSelectorKey, false).ToUnit(); 24 | 25 | // add a throttle if specified 26 | if (throttle is not null) 27 | { 28 | regrouper = regrouper.Throttle(throttle.Value, scheduler ?? GlobalConfig.DefaultScheduler); 29 | } 30 | 31 | // Use property changes as a trigger to re-evaluate Grouping 32 | return shared.GroupOn(_groupSelector, regrouper); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/GroupOnPropertyWithImmutableState.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | using System.Linq.Expressions; 7 | using System.Reactive.Concurrency; 8 | using System.Reactive.Linq; 9 | 10 | namespace DynamicData.List.Internal; 11 | 12 | internal sealed class GroupOnPropertyWithImmutableState(IObservable> source, Expression> groupSelectorKey, TimeSpan? throttle = null, IScheduler? scheduler = null) 13 | where TObject : INotifyPropertyChanged 14 | where TGroup : notnull 15 | { 16 | private readonly Func _groupSelector = groupSelectorKey.Compile(); 17 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 18 | 19 | public IObservable>> Run() => _source.Publish( 20 | shared => 21 | { 22 | // Monitor explicit property changes 23 | var regrouper = shared.WhenValueChanged(groupSelectorKey, false).ToUnit(); 24 | 25 | // add a throttle if specified 26 | if (throttle is not null) 27 | { 28 | regrouper = regrouper.Throttle(throttle.Value, scheduler ?? GlobalConfig.DefaultScheduler); 29 | } 30 | 31 | // Use property changes as a trigger to re-evaluate Grouping 32 | return shared.GroupWithImmutableState(_groupSelector, regrouper); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/LimitSizeTo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Concurrency; 6 | using System.Reactive.Linq; 7 | 8 | namespace DynamicData.List.Internal; 9 | 10 | #if NET9_0_OR_GREATER 11 | internal sealed class LimitSizeTo(ISourceList sourceList, int sizeLimit, IScheduler scheduler, Lock locker) 12 | #else 13 | internal sealed class LimitSizeTo(ISourceList sourceList, int sizeLimit, IScheduler scheduler, object locker) 14 | #endif 15 | 16 | where T : notnull 17 | { 18 | private readonly IScheduler _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler)); 19 | private readonly ISourceList _sourceList = sourceList ?? throw new ArgumentNullException(nameof(sourceList)); 20 | 21 | public IObservable> Run() 22 | { 23 | var emptyResult = new List(); 24 | long orderItemWasAdded = -1; 25 | 26 | return _sourceList.Connect().ObserveOn(_scheduler).Synchronize(locker).Transform(t => new ExpirableItem(t, _scheduler.Now.UtcDateTime, Interlocked.Increment(ref orderItemWasAdded))).ToCollection().Select( 27 | list => 28 | { 29 | var numberToExpire = list.Count - sizeLimit; 30 | if (numberToExpire < 0) 31 | { 32 | return emptyResult; 33 | } 34 | 35 | return list.OrderBy(exp => exp.ExpireAt).ThenBy(exp => exp.Index).Take(numberToExpire).Select(item => item.Item).ToList(); 36 | }).Where(items => items.Count != 0); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/OnBeingAdded.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.List.Internal; 8 | 9 | internal sealed class OnBeingAdded(IObservable> source, Action callback) 10 | where T : notnull 11 | { 12 | private readonly Action _callback = callback ?? throw new ArgumentNullException(nameof(callback)); 13 | 14 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 15 | 16 | public IObservable> Run() => _source.Do(RegisterForAddition); 17 | 18 | private void RegisterForAddition(IChangeSet changes) 19 | { 20 | foreach (var change in changes) 21 | { 22 | switch (change.Reason) 23 | { 24 | case ListChangeReason.Add: 25 | _callback(change.Item.Current); 26 | break; 27 | 28 | case ListChangeReason.AddRange: 29 | change.Range.ForEach(_callback); 30 | break; 31 | 32 | case ListChangeReason.Replace: 33 | _callback(change.Item.Current); 34 | break; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/QueryWhenChanged.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Linq; 6 | 7 | namespace DynamicData.List.Internal; 8 | 9 | internal sealed class QueryWhenChanged(IObservable> source) 10 | where T : notnull 11 | { 12 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 13 | 14 | public IObservable> Run() => Observable.Create>(observer => 15 | { 16 | var list = new List(); 17 | 18 | return _source.Subscribe(changes => 19 | { 20 | list.Clone(changes); 21 | observer.OnNext(new ReadOnlyCollectionLight(list)); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/SubscribeMany.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive; 6 | using System.Reactive.Disposables; 7 | using System.Reactive.Linq; 8 | 9 | namespace DynamicData.List.Internal; 10 | 11 | internal sealed class SubscribeMany(IObservable> source, Func subscriptionFactory) 12 | where T : notnull 13 | { 14 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 15 | 16 | private readonly Func _subscriptionFactory = subscriptionFactory ?? throw new ArgumentNullException(nameof(subscriptionFactory)); 17 | 18 | public IObservable> Run() => Observable.Create>( 19 | observer => 20 | { 21 | var shared = _source.Publish(); 22 | var subscriptions = shared 23 | .Transform(t => _subscriptionFactory(t)) 24 | .DisposeMany() 25 | .SubscribeSafe(Observer.Create>( 26 | onNext: static _ => { }, 27 | onError: observer.OnError, 28 | onCompleted: static () => { })); 29 | 30 | return new CompositeDisposable(subscriptions, shared.SubscribeSafe(observer), shared.Connect()); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/DynamicData/List/Internal/Switch.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | 8 | namespace DynamicData.List.Internal; 9 | 10 | internal sealed class Switch(IObservable>> sources) 11 | where T : notnull 12 | { 13 | private readonly IObservable>> _sources = sources ?? throw new ArgumentNullException(nameof(sources)); 14 | 15 | public IObservable> Run() => Observable.Create>( 16 | observer => 17 | { 18 | var locker = InternalEx.NewLock(); 19 | 20 | var destination = new SourceList(); 21 | 22 | var populator = Observable.Switch( 23 | _sources.Do( 24 | _ => 25 | { 26 | lock (locker) 27 | { 28 | destination.Clear(); 29 | } 30 | })).Synchronize(locker).PopulateInto(destination); 31 | 32 | var publisher = destination.Connect().SubscribeSafe(observer); 33 | return new CompositeDisposable(destination, populator, publisher); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/DynamicData/List/Linq/WithoutIndexEnumerator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections; 6 | 7 | namespace DynamicData.List.Linq; 8 | 9 | /// 10 | /// Index to remove the index. This is necessary for WhereReasonAre* operators. 11 | /// Otherwise these operators could break subsequent operators when the subsequent operator relies on the index. 12 | /// 13 | /// The type of the item. 14 | internal sealed class WithoutIndexEnumerator(IEnumerable> changeSet) : IEnumerable> 15 | where T : notnull 16 | { 17 | public IEnumerator> GetEnumerator() 18 | { 19 | foreach (var change in changeSet) 20 | { 21 | if (change.Reason == ListChangeReason.Moved) 22 | { 23 | // exceptional case - makes no sense to remove index from move 24 | continue; 25 | } 26 | 27 | if (change.Type == ChangeType.Item) 28 | { 29 | yield return new Change(change.Reason, change.Item.Current, change.Item.Previous); 30 | } 31 | else 32 | { 33 | yield return new Change(change.Reason, change.Range); 34 | } 35 | } 36 | } 37 | 38 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 39 | } 40 | -------------------------------------------------------------------------------- /src/DynamicData/List/ListChangeReason.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// The reason for an individual change to an observable list. 9 | /// Used to signal consumers of any changes to the underlying cache. 10 | /// 11 | public enum ListChangeReason 12 | { 13 | /// 14 | /// An item has been added. 15 | /// 16 | Add, 17 | 18 | /// 19 | /// A range of items has been added. 20 | /// 21 | AddRange, 22 | 23 | /// 24 | /// An item has been replaced. 25 | /// 26 | Replace, 27 | 28 | /// 29 | /// An item has removed. 30 | /// 31 | Remove, 32 | 33 | /// 34 | /// A range of items has been removed. 35 | /// 36 | RemoveRange, 37 | 38 | /// 39 | /// Command to operators to re-evaluate. 40 | /// 41 | Refresh, 42 | 43 | /// 44 | /// An item has been moved in a sorted collection. 45 | /// 46 | Moved, 47 | 48 | /// 49 | /// The entire collection has been cleared. 50 | /// 51 | Clear, 52 | } 53 | -------------------------------------------------------------------------------- /src/DynamicData/List/ListFilterPolicy.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Specifies which filter strategy should be used when the filter predicate is changed. 9 | /// 10 | public enum ListFilterPolicy 11 | { 12 | /// 13 | /// Clear all items and replace with matches - optimised for large data sets. 14 | /// This option preserves order. 15 | /// 16 | ClearAndReplace, 17 | 18 | /// 19 | /// Calculate diff set - optimised for general filtering. 20 | /// This option does not preserve order. 21 | /// 22 | CalculateDiff 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicData/List/PageChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections; 6 | 7 | using DynamicData.Operators; 8 | 9 | // ReSharper disable once CheckNamespace 10 | namespace DynamicData; 11 | 12 | internal sealed class PageChangeSet(IChangeSet virtualChangeSet, IPageResponse response) : IPageChangeSet 13 | where T : notnull 14 | { 15 | private readonly IChangeSet _virtualChangeSet = virtualChangeSet ?? throw new ArgumentNullException(nameof(virtualChangeSet)); 16 | 17 | public int Count => _virtualChangeSet.Count; 18 | 19 | public int Refreshes => _virtualChangeSet.Refreshes; 20 | 21 | public IPageResponse Response { get; } = response ?? throw new ArgumentNullException(nameof(response)); 22 | 23 | int IChangeSet.Adds => _virtualChangeSet.Adds; 24 | 25 | int IChangeSet.Capacity 26 | { 27 | get => _virtualChangeSet.Capacity; 28 | set => _virtualChangeSet.Capacity = value; 29 | } 30 | 31 | int IChangeSet.Moves => _virtualChangeSet.Moves; 32 | 33 | int IChangeSet.Removes => _virtualChangeSet.Removes; 34 | 35 | int IChangeSet.Replaced => _virtualChangeSet.Replaced; 36 | 37 | int IChangeSet.TotalChanges => _virtualChangeSet.TotalChanges; 38 | 39 | public IEnumerator> GetEnumerator() => _virtualChangeSet.GetEnumerator(); 40 | 41 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 42 | } 43 | -------------------------------------------------------------------------------- /src/DynamicData/List/SortException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// Thrown when an exception occurs within the sort operators. 10 | /// 11 | [Serializable] 12 | public class SortException : Exception 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The message that describes the error. 18 | public SortException(string message) 19 | : base(message) 20 | { 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | public SortException() 27 | { 28 | } 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// A message about the exception. 34 | /// A inner exception with further information. 35 | public SortException(string message, Exception innerException) 36 | : base(message, innerException) 37 | { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DynamicData/List/SortOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Options for sorting. 9 | /// 10 | public enum SortOptions 11 | { 12 | /// 13 | /// No sort options are specified. 14 | /// 15 | None, 16 | 17 | /// 18 | /// Use binary search to locate item index. 19 | /// 20 | UseBinarySearch 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/List/SourceListEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// Source list extensions. 10 | /// 11 | public static class SourceListEx 12 | { 13 | /// 14 | /// Connects to the list, and converts the changes to another form. 15 | /// Alas, I had to add the converter due to type inference issues. 16 | /// 17 | /// The type of the object. 18 | /// The type of the destination. 19 | /// The source. 20 | /// The conversion factory. 21 | /// An observable which emits that change set. 22 | public static IObservable> Cast(this ISourceList source, Func conversionFactory) 23 | where TSource : notnull 24 | where TDestination : notnull 25 | { 26 | source.ThrowArgumentNullExceptionIfNull(nameof(source)); 27 | conversionFactory.ThrowArgumentNullExceptionIfNull(nameof(conversionFactory)); 28 | 29 | return source.Connect().Cast(conversionFactory); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DynamicData/List/Tests/ListTextEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData.Tests; 7 | 8 | /// 9 | /// Test extensions. 10 | /// 11 | public static class ListTextEx 12 | { 13 | /// 14 | /// Aggregates all events and statistics for a change set to help assertions when testing. 15 | /// 16 | /// The source observable. 17 | /// The type of the object. 18 | /// The change set aggregator. 19 | public static ChangeSetAggregator AsAggregator(this IObservable> source) 20 | where T : notnull => new(source); 21 | } 22 | -------------------------------------------------------------------------------- /src/DynamicData/List/UnspecifiedIndexException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace DynamicData; 7 | 8 | /// 9 | /// Thrown when an index is expected but not specified. 10 | /// 11 | [Serializable] 12 | public class UnspecifiedIndexException : Exception 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The message that describes the error. 18 | public UnspecifiedIndexException(string message) 19 | : base(message) 20 | { 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | public UnspecifiedIndexException() 27 | { 28 | } 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// The error message that explains the reason for the exception. 34 | /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. 35 | public UnspecifiedIndexException(string message, Exception innerException) 36 | : base(message, innerException) 37 | { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DynamicData/List/VirtualChangeSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace DynamicData; 9 | 10 | internal sealed class VirtualChangeSet(IChangeSet virtualChangeSet, IVirtualResponse response) : IVirtualChangeSet 11 | where T : notnull 12 | { 13 | private readonly IChangeSet _virtualChangeSet = virtualChangeSet ?? throw new ArgumentNullException(nameof(virtualChangeSet)); 14 | 15 | public int Refreshes => _virtualChangeSet.Refreshes; 16 | 17 | public IVirtualResponse Response { get; } = response ?? throw new ArgumentNullException(nameof(response)); 18 | 19 | int IChangeSet.Adds => _virtualChangeSet.Adds; 20 | 21 | int IChangeSet.Capacity 22 | { 23 | get => _virtualChangeSet.Capacity; 24 | set => _virtualChangeSet.Capacity = value; 25 | } 26 | 27 | int IChangeSet.Count => _virtualChangeSet.Count; 28 | 29 | int IChangeSet.Moves => _virtualChangeSet.Moves; 30 | 31 | int IChangeSet.Removes => _virtualChangeSet.Removes; 32 | 33 | int IChangeSet.Replaced => _virtualChangeSet.Replaced; 34 | 35 | int IChangeSet.TotalChanges => _virtualChangeSet.TotalChanges; 36 | 37 | public IEnumerator> GetEnumerator() => _virtualChangeSet.GetEnumerator(); 38 | 39 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 40 | } 41 | -------------------------------------------------------------------------------- /src/DynamicData/ObsoleteEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace DynamicData; 6 | 7 | /// 8 | /// Obsolete methods: Kept in system to prevent breaking changes for now. 9 | /// 10 | public static class ObsoleteEx 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/DynamicData/Platforms/net45/PSubscribeMany.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | #if P_LINQ 6 | using System.Reactive.Disposables; 7 | using System.Reactive.Linq; 8 | 9 | // ReSharper disable once CheckNamespace 10 | namespace DynamicData.PLinq 11 | { 12 | internal sealed class PSubscribeMany(IObservable> source, Func subscriptionFactory, ParallelisationOptions parallelisationOptions) 13 | where TObject : notnull 14 | where TKey : notnull 15 | { 16 | private readonly IObservable> _source = source ?? throw new ArgumentNullException(nameof(source)); 17 | 18 | private readonly Func _subscriptionFactory = subscriptionFactory ?? throw new ArgumentNullException(nameof(subscriptionFactory)); 19 | 20 | public IObservable> Run() => Observable.Create>( 21 | observer => 22 | { 23 | var published = _source.Publish(); 24 | var subscriptions = published.Transform((t, k) => _subscriptionFactory(t, k), parallelisationOptions).DisposeMany().Subscribe(); 25 | 26 | return new CompositeDisposable(subscriptions, published.SubscribeSafe(observer), published.Connect()); 27 | }); 28 | } 29 | } 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/DynamicData/Platforms/net45/ParallelType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | #if P_LINQ 6 | // ReSharper disable once CheckNamespace 7 | namespace DynamicData.PLinq 8 | { 9 | /// 10 | /// The type of parallelisation. 11 | /// 12 | public enum ParallelType 13 | { 14 | /// 15 | /// No parallelisation will take place. 16 | /// 17 | None, 18 | 19 | /// 20 | /// Parallelisation will take place without preserving the enumerable order. 21 | /// 22 | Parallelise, 23 | 24 | /// 25 | /// Parallelisation will take place whilst preserving the enumerable order. 26 | /// 27 | Ordered 28 | } 29 | } 30 | 31 | #endif -------------------------------------------------------------------------------- /src/DynamicData/Polyfills/CompilerFeatureRequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | #if !NET7_0_OR_GREATER 5 | namespace System.Runtime.CompilerServices; 6 | 7 | // Allows use of the C#11 `required` keyword, internally within this library, when targeting frameworks older than .NET 7. 8 | [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] 9 | internal sealed class CompilerFeatureRequiredAttribute(string featureName) 10 | : Attribute 11 | { 12 | public const string RefStructs 13 | = nameof(RefStructs); 14 | 15 | public const string RequiredMembers 16 | = nameof(RequiredMembers); 17 | 18 | public string FeatureName { get; } = featureName; 19 | 20 | public bool IsOptional { get; init; } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /src/DynamicData/Polyfills/DynamicallyAccessedMembersAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | #if !NET5_0_OR_GREATER 6 | namespace System.Diagnostics.CodeAnalysis; 7 | 8 | [Flags] 9 | internal enum DynamicallyAccessedMemberTypes 10 | { 11 | All = -1, 12 | None = 0, 13 | PublicParameterlessConstructor = 1, 14 | PublicConstructors = 3, 15 | NonPublicConstructors = 4, 16 | PublicMethods = 8, 17 | NonPublicMethods = 16, 18 | PublicFields = 32, 19 | NonPublicFields = 64, 20 | PublicNestedTypes = 128, 21 | NonPublicNestedTypes = 256, 22 | PublicProperties = 512, 23 | NonPublicProperties = 1024, 24 | PublicEvents = 2048, 25 | NonPublicEvents = 4096, 26 | Interfaces = 8192 27 | } 28 | 29 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, Inherited = false)] 30 | internal sealed class DynamicallyAccessedMembersAttribute : Attribute 31 | { 32 | public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) 33 | { 34 | MemberTypes = memberTypes; 35 | } 36 | 37 | public DynamicallyAccessedMemberTypes MemberTypes { get; } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /src/DynamicData/Polyfills/EnumEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace System; 6 | 7 | internal static class EnumEx 8 | { 9 | #if NET5_0_OR_GREATER 10 | public static bool IsDefined(TEnum value) 11 | where TEnum : struct, Enum 12 | => Enum.IsDefined(value); 13 | #else 14 | public static bool IsDefined(TEnum value) 15 | where TEnum : struct, Enum 16 | => Enum.IsDefined(typeof(TEnum), value); 17 | #endif 18 | } 19 | -------------------------------------------------------------------------------- /src/DynamicData/Polyfills/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | #if !NETCOREAPP 5 | namespace System.Runtime.CompilerServices; 6 | 7 | // Allows use of the C#11 `init` keyword, internally within this library, when targeting frameworks older than .NET 5. 8 | internal sealed class IsExternalInit; 9 | #endif 10 | -------------------------------------------------------------------------------- /src/DynamicData/Polyfills/ListEnsureCapacity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | #if !NETCOREAPP 5 | namespace System.Collections.Generic; 6 | 7 | internal static class ListEnsureCapacity 8 | { 9 | public static void EnsureCapacity(this List list, int capacity) 10 | { 11 | if (list.Capacity < capacity) 12 | list.Capacity = capacity; 13 | } 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /src/DynamicData/Polyfills/RequiredMemberAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. 2 | // Roland Pheasant licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | #if !NET7_0_OR_GREATER 5 | using System.ComponentModel; 6 | 7 | namespace System.Runtime.CompilerServices; 8 | 9 | // Allows use of the C#11 `required` keyword, internally within this library, when targeting frameworks older than .NET 7. 10 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, Inherited = false)] 11 | [EditorBrowsable(EditorBrowsableState.Never)] 12 | internal sealed class RequiredMemberAttribute : Attribute; 13 | #endif 14 | -------------------------------------------------------------------------------- /src/coverlet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | opencover 8 | [DynamicData]* 9 | Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute 10 | false 11 | 12 | true 13 | false 14 | MissingAll,MissingAny,None 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild-sdks": { 3 | "MSBuild.Sdk.Extras": "3.0.44" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "useTabs": false, 6 | "indentationSize": 4 7 | }, 8 | "documentationRules": { 9 | "documentExposedElements": true, 10 | "documentInternalElements": false, 11 | "documentPrivateElements": false, 12 | "documentInterfaces": true, 13 | "documentPrivateFields": false, 14 | "documentationCulture": "en-US", 15 | "companyName": "Roland Pheasant", 16 | "copyrightText": "Copyright (c) 2011-2023 {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.", 17 | "variables": { 18 | "licenseName": "MIT", 19 | "licenseFile": "LICENSE" 20 | }, 21 | "xmlHeader": false 22 | }, 23 | "layoutRules": { 24 | "newlineAtEndOfFile": "allow", 25 | "allowConsecutiveUsings": true 26 | }, 27 | "maintainabilityRules": { 28 | "topLevelTypes": [ 29 | "class", 30 | "interface", 31 | "struct", 32 | "enum", 33 | "delegate" 34 | ] 35 | }, 36 | "orderingRules": { 37 | "usingDirectivesPlacement": "outsideNamespace", 38 | "systemUsingDirectivesFirst": true 39 | }, 40 | "namingRules": { 41 | "tupleElementNameCasing": "camelCase" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "9.3", 3 | "publicReleaseRefSpec": [ 4 | "^refs/heads/main$", // we release out of master 5 | "^refs/heads/preview/.*", // we release previews 6 | "^refs/heads/rel/\\d+\\.\\d+\\.\\d+" // we also release branches starting with rel/N.N.N 7 | ], 8 | "nugetPackageVersion": { 9 | "semVer": 2 10 | }, 11 | "cloudBuild": { 12 | "setVersionVariables": true, 13 | "buildNumber": { 14 | "enabled": false 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------