├── .gitignore ├── DynamicData.Snippets.sln ├── DynamicData.Snippets ├── Aggregation │ ├── AggregationFixture.cs │ └── Aggregations.cs ├── AutoRefresh │ ├── AutoRefreshChain.cs │ ├── AutoRefreshFixture.cs │ └── AutoRefreshForPropertyChanges.cs ├── Creation │ ├── ChangeSetCreation.cs │ └── CreationFixture.cs ├── DynamicData.Snippets.csproj ├── Filter │ ├── DynamicFilter.cs │ ├── ExternalSourceFilter.cs │ ├── FilterFixture.cs │ ├── PropertyFilter.cs │ └── StaticFilter.cs ├── Group │ ├── CustomTotalRows.cs │ ├── GroupAndMonitorPropertyChanges.cs │ ├── GroupByWeek.cs │ ├── GroupFixture.cs │ └── XamarinFormsGrouping.cs ├── Infrastructure │ ├── Animal.cs │ ├── DynamicDataEx.cs │ ├── ISchedulerProvider.cs │ ├── ObservableEx.cs │ ├── StringEx.cs │ └── TestSchedulerProvider.cs ├── InspectItems │ ├── InspectCollection.cs │ ├── InspectCollectionWithObservable.cs │ ├── InspectCollectionWithPropertyChanges.cs │ ├── InspectItemsFixture.cs │ ├── KeepItemSelected.cs │ └── MonitorSelectedItems.cs ├── Join │ └── JoinBasedOnListOfIds.cs ├── Paging │ ├── PagingFixture.cs │ └── SimplePagging.cs ├── Sorting │ ├── ChangeComparer.cs │ ├── CustomBinding.cs │ ├── SortFixture.cs │ └── VariableThresholdObservableCollectionAdaptor.cs ├── Switch │ ├── SwitchDataSource.cs │ └── SwitchDataSourceFixture.cs ├── Transform │ ├── FlattenNestedObservableCollection.cs │ └── TransformFixture.cs ├── ViewModelTesting │ ├── ViewModel.cs │ └── ViewModelFixture.cs ├── Virtualise │ ├── PagingListWithVirtualise.cs │ └── PagingListWithVirtualiseFixture.cs └── Watch │ ├── SelectCacheItem.cs │ └── SelectCacheItemFixture.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/visualstudio 3 | 4 | ### VisualStudio ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | ## 8 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | [Ll]og/ 30 | 31 | # Visual Studio 2015 cache/options directory 32 | .vs/ 33 | # Uncomment if you have tasks that create the project's static files in wwwroot 34 | #wwwroot/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # .NET Core 50 | project.lock.json 51 | project.fragment.lock.json 52 | artifacts/ 53 | **/Properties/launchSettings.json 54 | 55 | *_i.c 56 | *_p.c 57 | *_i.h 58 | *.ilk 59 | *.meta 60 | *.obj 61 | *.pch 62 | *.pdb 63 | *.pgc 64 | *.pgd 65 | *.rsp 66 | *.sbr 67 | *.tlb 68 | *.tli 69 | *.tlh 70 | *.tmp 71 | *.tmp_proj 72 | *.log 73 | *.vspscc 74 | *.vssscc 75 | .builds 76 | *.pidb 77 | *.svclog 78 | *.scc 79 | 80 | # Chutzpah Test files 81 | _Chutzpah* 82 | 83 | # Visual C++ cache files 84 | ipch/ 85 | *.aps 86 | *.ncb 87 | *.opendb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | *.VC.db 92 | *.VC.VC.opendb 93 | 94 | # Visual Studio profiler 95 | *.psess 96 | *.vsp 97 | *.vspx 98 | *.sap 99 | 100 | # TFS 2012 Local Workspace 101 | $tf/ 102 | 103 | # Guidance Automation Toolkit 104 | *.gpState 105 | 106 | # ReSharper is a .NET coding add-in 107 | _ReSharper*/ 108 | *.[Rr]e[Ss]harper 109 | *.DotSettings.user 110 | 111 | # JustCode is a .NET coding add-in 112 | .JustCode 113 | 114 | # TeamCity is a build add-in 115 | _TeamCity* 116 | 117 | # DotCover is a Code Coverage Tool 118 | *.dotCover 119 | 120 | # Visual Studio code coverage results 121 | *.coverage 122 | *.coveragexml 123 | 124 | # NCrunch 125 | _NCrunch_* 126 | .*crunch*.local.xml 127 | nCrunchTemp_* 128 | 129 | # MightyMoose 130 | *.mm.* 131 | AutoTest.Net/ 132 | 133 | # Web workbench (sass) 134 | .sass-cache/ 135 | 136 | # Installshield output folder 137 | [Ee]xpress/ 138 | 139 | # DocProject is a documentation generator add-in 140 | DocProject/buildhelp/ 141 | DocProject/Help/*.HxT 142 | DocProject/Help/*.HxC 143 | DocProject/Help/*.hhc 144 | DocProject/Help/*.hhk 145 | DocProject/Help/*.hhp 146 | DocProject/Help/Html2 147 | DocProject/Help/html 148 | 149 | # Click-Once directory 150 | publish/ 151 | 152 | # Publish Web Output 153 | *.[Pp]ublish.xml 154 | *.azurePubxml 155 | # TODO: Comment the next line if you want to checkin your web deploy settings 156 | # but database connection strings (with potential passwords) will be unencrypted 157 | *.pubxml 158 | *.publishproj 159 | 160 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 161 | # checkin your Azure Web App publish settings, but sensitive information contained 162 | # in these scripts will be unencrypted 163 | PublishScripts/ 164 | 165 | # NuGet Packages 166 | *.nupkg 167 | # The packages folder can be ignored because of Package Restore 168 | **/packages/* 169 | # except build/, which is used as an MSBuild target. 170 | !**/packages/build/ 171 | # Uncomment if necessary however generally it will be regenerated when needed 172 | #!**/packages/repositories.config 173 | # NuGet v3's project.json files produces more ignorable files 174 | *.nuget.props 175 | *.nuget.targets 176 | 177 | # Microsoft Azure Build Output 178 | csx/ 179 | *.build.csdef 180 | 181 | # Microsoft Azure Emulator 182 | ecf/ 183 | rcf/ 184 | 185 | # Windows Store app package directories and files 186 | AppPackages/ 187 | BundleArtifacts/ 188 | Package.StoreAssociation.xml 189 | _pkginfo.txt 190 | 191 | # Visual Studio cache files 192 | # files ending in .cache can be ignored 193 | *.[Cc]ache 194 | # but keep track of directories ending in .cache 195 | !*.[Cc]ache/ 196 | 197 | # Others 198 | ClientBin/ 199 | ~$* 200 | *~ 201 | *.dbmdl 202 | *.dbproj.schemaview 203 | *.jfm 204 | *.pfx 205 | *.publishsettings 206 | orleans.codegen.cs 207 | 208 | # Since there are multiple workflows, uncomment next line to ignore bower_components 209 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 210 | #bower_components/ 211 | 212 | # RIA/Silverlight projects 213 | Generated_Code/ 214 | 215 | # Backup & report files from converting an old project file 216 | # to a newer Visual Studio version. Backup files are not needed, 217 | # because we have git ;-) 218 | _UpgradeReport_Files/ 219 | Backup*/ 220 | UpgradeLog*.XML 221 | UpgradeLog*.htm 222 | 223 | # SQL Server files 224 | *.mdf 225 | *.ldf 226 | *.ndf 227 | 228 | # Business Intelligence projects 229 | *.rdl.data 230 | *.bim.layout 231 | *.bim_*.settings 232 | 233 | # Microsoft Fakes 234 | FakesAssemblies/ 235 | 236 | # GhostDoc plugin setting file 237 | *.GhostDoc.xml 238 | 239 | # Node.js Tools for Visual Studio 240 | .ntvs_analysis.dat 241 | node_modules/ 242 | 243 | # Typescript v1 declaration files 244 | typings/ 245 | 246 | # Visual Studio 6 build log 247 | *.plg 248 | 249 | # Visual Studio 6 workspace options file 250 | *.opt 251 | 252 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 253 | *.vbw 254 | 255 | # Visual Studio LightSwitch build output 256 | **/*.HTMLClient/GeneratedArtifacts 257 | **/*.DesktopClient/GeneratedArtifacts 258 | **/*.DesktopClient/ModelManifest.xml 259 | **/*.Server/GeneratedArtifacts 260 | **/*.Server/ModelManifest.xml 261 | _Pvt_Extensions 262 | 263 | # Paket dependency manager 264 | .paket/paket.exe 265 | paket-files/ 266 | 267 | # FAKE - F# Make 268 | .fake/ 269 | 270 | # JetBrains Rider 271 | .idea/ 272 | *.sln.iml 273 | 274 | # CodeRush 275 | .cr/ 276 | 277 | # Python Tools for Visual Studio (PTVS) 278 | __pycache__/ 279 | *.pyc 280 | 281 | # Cake - Uncomment if you are using it 282 | # tools/** 283 | # !tools/packages.config 284 | 285 | # Telerik's JustMock configuration file 286 | *.jmconfig 287 | 288 | # BizTalk build output 289 | *.btp.cs 290 | *.btm.cs 291 | *.odx.cs 292 | *.xsd.cs 293 | 294 | # End of https://www.gitignore.io/api/visualstudio 295 | *.DotSettings 296 | -------------------------------------------------------------------------------- /DynamicData.Snippets.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamicData.Snippets", "DynamicData.Snippets\DynamicData.Snippets.csproj", "{E8204D00-1455-47D7-B866-7380ED3007D9}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{98FA418F-A164-4FCE-8C9F-D36AA07653DE}" 9 | ProjectSection(SolutionItems) = preProject 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {E8204D00-1455-47D7-B866-7380ED3007D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {E8204D00-1455-47D7-B866-7380ED3007D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {E8204D00-1455-47D7-B866-7380ED3007D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {E8204D00-1455-47D7-B866-7380ED3007D9}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {49BD3A2A-961B-48A9-86EA-2904DBD50073} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Aggregation/AggregationFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace DynamicData.Snippets.Aggregation 6 | { 7 | public class AggregationFixture 8 | { 9 | [Fact] 10 | public void Aggregations() 11 | { 12 | using (var dataSource = new SourceList()) 13 | using (var sut = new Aggregations(dataSource)) 14 | { 15 | //check StartWithEmpty() has taken effect 16 | sut.Min.Should().NotBeNull(); 17 | sut.Max.Should().NotBeNull(); 18 | sut.Avg.Should().NotBeNull(); 19 | 20 | dataSource.AddRange(Enumerable.Range(1, 10)); 21 | 22 | sut.Min.Should().Be(1); 23 | sut.Max.Should().Be(10); 24 | sut.Avg.Should().Be(5.5); 25 | 26 | dataSource.RemoveRange(0,9); 27 | dataSource.Add(100); 28 | 29 | //items in list = [10,100] 30 | sut.Min.Should().Be(10); 31 | sut.Max.Should().Be(100); 32 | sut.Avg.Should().Be(55); 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/Aggregation/Aggregations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Disposables; 3 | using System.Reactive.Linq; 4 | 5 | //MUST INCLUDE NAMESPACE FOR AGGREGATIONS 6 | using DynamicData.Aggregation; 7 | 8 | namespace DynamicData.Snippets.Aggregation 9 | { 10 | public class Aggregations : IDisposable 11 | { 12 | private readonly IDisposable _cleanUp; 13 | 14 | public int? Min { get; set; } 15 | public int? Max { get; set; } 16 | public double? Avg { get; set; } 17 | 18 | public Aggregations(IObservableList source) 19 | { 20 | /* 21 | * Available aggregations: Max, Min, Avg, StdDev, Count, Sum. 22 | * 23 | * For custom aggregations use: source.Connect().ToCollection().Select(items=>...); 24 | */ 25 | var shared = source.Connect() 26 | //by default dd never notifies when the change set is empty i.e. upon subscripion when the source has no data 27 | //this means that no result is computed until data is loaded. However if you require a result even when the data source is empty, use StartWithEmpty() 28 | .StartWithEmpty() 29 | //use standard rx Publish() / Connect() to share published changesets 30 | .Publish(); 31 | 32 | _cleanUp = new CompositeDisposable 33 | ( 34 | shared.Maximum(i => i).Subscribe(max => Max = max), 35 | shared.Minimum(i => i).Subscribe(min => Min = min), 36 | shared.Avg(i => i).Subscribe(avg => Avg = avg), 37 | 38 | shared.Connect() 39 | ); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | _cleanUp.Dispose(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DynamicData.Snippets/AutoRefresh/AutoRefreshChain.cs: -------------------------------------------------------------------------------- 1 | using DynamicData.Binding; 2 | 3 | namespace DynamicData.Snippets.AutoRefresh 4 | { 5 | public class AutoRefreshChain 6 | { 7 | } 8 | 9 | public class Grade :AbstractNotifyPropertyChanged 10 | { 11 | private string _name; 12 | private int _score; 13 | private string _class; 14 | 15 | public Grade(int id, string name, string @class) 16 | { 17 | Id = id; 18 | Name = name; 19 | } 20 | 21 | public int Id { get; } 22 | 23 | public string Name 24 | { 25 | get => _name; 26 | set => SetAndRaise(ref _name, value); 27 | } 28 | 29 | public string Class 30 | { 31 | get => _class; 32 | set => SetAndRaise(ref _class, value); 33 | } 34 | 35 | public int Score 36 | { 37 | get => _score; 38 | set => SetAndRaise(ref _score, value); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DynamicData.Snippets/AutoRefresh/AutoRefreshFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace DynamicData.Snippets.AutoRefresh 7 | { 8 | public class AutoRefreshFixture 9 | { 10 | [Fact] 11 | public void AutoRefresh() 12 | { 13 | var items = new List 14 | { 15 | new MutableThing(1, "A"), 16 | new MutableThing(2, "A"), 17 | new MutableThing(3, "B"), 18 | new MutableThing(4, "C"), 19 | new MutableThing(5, "D"), 20 | new MutableThing(6, "D"), 21 | 22 | }; 23 | //result should only be true when all items are set to true 24 | using (var cache = new SourceCache(m => m.Id)) 25 | { 26 | var sut = new AutoRefreshForPropertyChanges(cache); 27 | 28 | int count = 0; 29 | sut.DistinctCount.Subscribe(result => count = result); 30 | 31 | cache.AddOrUpdate(items); 32 | count.Should().Be(4); 33 | 34 | //check mutating a value works 35 | items[2].Value = "A"; 36 | count.Should().Be(3); 37 | 38 | //check remove works 39 | cache.RemoveKey(4); 40 | count.Should().Be(2); 41 | 42 | //check add works 43 | cache.AddOrUpdate(new MutableThing(10, "z")); 44 | count.Should().Be(3); 45 | 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DynamicData.Snippets/AutoRefresh/AutoRefreshForPropertyChanges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DynamicData.Binding; 3 | using DynamicData.Snippets.Infrastructure; 4 | 5 | namespace DynamicData.Snippets.AutoRefresh 6 | { 7 | public class AutoRefreshForPropertyChanges 8 | { 9 | 10 | public IObservable DistinctCount { get; } 11 | 12 | public AutoRefreshForPropertyChanges(IObservableCache dataSource) 13 | { 14 | /* 15 | * The observable cache has no concept of mutable properties. It only knows about adds, updates and removes. 16 | * However it does have the concept of a refresh, which is a manual way to tell downstream operators like 17 | * distinct, sort, filter and grouping to re-evaluate 18 | * 19 | * To force a refresh, you need to manually trigger when to do so. In this case property changes are monitored 20 | * and the data source sends a refresh signal to all downstream operators. 21 | */ 22 | DistinctCount = dataSource.Connect() 23 | .AutoRefresh(t=>t.Value) //Omit param to refresh for any property 24 | .DistinctValues(m => m.Value) 25 | .Count(); 26 | } 27 | } 28 | 29 | public class MutableThing : AbstractNotifyPropertyChanged 30 | { 31 | private string _value; 32 | 33 | public MutableThing(int id, string value) 34 | { 35 | Id = id; 36 | Value = value; 37 | } 38 | 39 | public int Id { get; } 40 | 41 | public string Value 42 | { 43 | get => _value; 44 | set => SetAndRaise(ref _value, value); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/Creation/ChangeSetCreation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive; 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace DynamicData.Snippets.Creation 10 | { 11 | /// 12 | /// Examples of creating observable change sets for the observable list. 13 | /// 14 | /// ObservableChangeSet.Create has exactly the same overloads as Observable.Create and has the same 15 | /// characteristics such as disposal of resources, error propagation and defered invocation i.e. when a subscription 16 | /// or ToObservableList is invoked. 17 | /// 18 | /// Using these overloads makes observable changes sets first class observablea and thus enable repeat / retry logic 19 | /// 20 | /// I have ommitted cache examples as the signature is identical except the cache has a key specified 21 | /// eg var observable = ObservableChangeSet.Create(cache=>{}, p=>p.Id); 22 | /// 23 | public static class ChangeSetCreation 24 | { 25 | /// 26 | /// Reload data and maintain list using edit diff which calculates a diff set from the previous load which can significantly reduce noise by poreventing 27 | /// unnecessary updates 28 | /// 29 | public static IObservable> ReloadableWithEditDiff(IObservable loadObservable, Func>> loader) 30 | { 31 | return ObservableChangeSet.Create(list => 32 | { 33 | return loadObservable 34 | .StartWith(Unit.Default) //ensure inital load 35 | .SelectMany(_ => loader()) 36 | .Subscribe(items => list.EditDiff(items, EqualityComparer.Default)); 37 | }); 38 | } 39 | 40 | /// 41 | /// Repeatedly reload data using Dynamic Data's Switch operator which will clear previous data 42 | /// and add newly loaded data 43 | /// 44 | public static IObservable> Reloadable(IObservable loadObservable) 45 | { 46 | return loadObservable 47 | .StartWith(Unit.Default) 48 | .Select(_ => FromTask()) 49 | .Switch(); 50 | } 51 | 52 | 53 | /// 54 | /// Create an observable change set from a task 55 | /// 56 | /// 57 | public static IObservable> FromTask() 58 | { 59 | return ObservableChangeSet.Create(async list => 60 | { 61 | var items = await LoadFromTask(); 62 | list.AddRange(items); 63 | return () => { }; 64 | }); 65 | } 66 | 67 | public static IObservable> FromTask(Func>> loader) 68 | { 69 | return ObservableChangeSet.Create(async list => 70 | { 71 | var items = await loader(); 72 | list.AddRange(items); 73 | return () => { }; 74 | }); 75 | } 76 | 77 | public static IObservable> FromTaskWithRefCount(Func>> loader) 78 | { 79 | //RefCount is a dd overload of the standard rx Publish().RefCount() operation 80 | //Do not use Publish() as dd automatically takes care of that 81 | return FromTask(loader).RefCount(); 82 | } 83 | 84 | /// 85 | /// Create an observable change set from 2 observables i) the initial load 2) a subscriber 86 | /// 87 | public static IObservable> FromObservable(IObservable> initialLoad, IObservable subscriptions) 88 | { 89 | return ObservableChangeSet.Create(list => 90 | { 91 | //in an enterprise app, would have to account for the gap between load and subscribe 92 | var initialSubscriber = initialLoad 93 | .Take(1) 94 | .Subscribe(list.AddRange); 95 | 96 | var subscriber = subscriptions 97 | .Subscribe(list.Add); 98 | 99 | return new CompositeDisposable(initialSubscriber, subscriber); 100 | }); 101 | } 102 | 103 | private static Task> LoadFromTask() 104 | { 105 | return Task.FromResult(Enumerable.Range(1, 10)); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Creation/CreationFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive; 5 | using System.Reactive.Linq; 6 | using System.Reactive.Subjects; 7 | using System.Threading.Tasks; 8 | using FluentAssertions; 9 | using Xunit; 10 | 11 | namespace DynamicData.Snippets.Creation 12 | { 13 | public class CreationFixture 14 | { 15 | [Fact] 16 | public void ListFromTask() 17 | { 18 | using (var sut = ChangeSetCreation.FromTask().AsObservableList()) 19 | { 20 | sut.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); 21 | } 22 | } 23 | 24 | [Fact] 25 | public void ListFromObservable() 26 | { 27 | var initial = new BehaviorSubject>(Enumerable.Range(1, 10)); 28 | var subscriptions = new Subject(); 29 | 30 | using (var sut = ChangeSetCreation.FromObservable(initial, subscriptions).AsObservableList()) 31 | { 32 | sut.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); 33 | 34 | subscriptions.OnNext(11); 35 | sut.Items.Should().BeEquivalentTo(Enumerable.Range(1, 11)); 36 | } 37 | } 38 | 39 | [Fact] 40 | public void Reloadable() 41 | { 42 | var loader = new Subject(); 43 | int loadCount = 0; 44 | 45 | using (var sut = ChangeSetCreation.Reloadable(loader) 46 | .Do(changes=> loadCount++) 47 | .AsObservableList()) 48 | { 49 | sut.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); 50 | loadCount.Should().Be(1); 51 | 52 | loader.OnNext(Unit.Default); 53 | sut.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); 54 | 55 | //the count will be 3 rather than 2 because a .Clear() is first called when an observable change set is switched 56 | loadCount.Should().Be(3); 57 | } 58 | } 59 | 60 | [Fact] 61 | public void LoadOnceWithRefcount() 62 | { 63 | int loadCount = 0; 64 | Task> Loader() 65 | { 66 | loadCount++; 67 | return Task.FromResult(Enumerable.Range(1, 10)); 68 | } 69 | 70 | //Ref 71 | var refcountSource = ChangeSetCreation.FromTaskWithRefCount(Loader); 72 | 73 | 74 | using (var sut1 = refcountSource.AsObservableList()) 75 | using (var sut2 = refcountSource.AsObservableList()) 76 | { 77 | sut1.Count.Should().Be(10); 78 | sut2.Count.Should().Be(10); 79 | } 80 | loadCount.Should().Be(1); 81 | 82 | using (var sut1 = refcountSource.AsObservableList()) 83 | using (var sut2 = refcountSource.AsObservableList()) 84 | using (var sut3 = refcountSource.AsObservableList()) 85 | using (var sut4 = refcountSource.AsObservableList()) 86 | using (var sut5 = refcountSource.AsObservableList()) 87 | { 88 | sut1.Count.Should().Be(10); 89 | sut2.Count.Should().Be(10); 90 | sut3.Count.Should().Be(10); 91 | sut4.Count.Should().Be(10); 92 | sut5.Count.Should().Be(10); 93 | } 94 | 95 | loadCount.Should().Be(2); 96 | } 97 | 98 | [Fact] 99 | public void ReloadableWithEditDiff() 100 | { 101 | var reloader = new Subject(); 102 | int loadCount = 0; 103 | IChangeSet lastChangeSet = null; 104 | 105 | Task> Loader() 106 | { 107 | loadCount++; 108 | return Task.FromResult(loadCount == 1 109 | ? Enumerable.Range(1, 10) 110 | : Enumerable.Range(1, 12)); 111 | } 112 | 113 | using (var sut = ChangeSetCreation.ReloadableWithEditDiff(reloader, Loader) 114 | .Do(changes => lastChangeSet = changes) 115 | .AsObservableList()) 116 | { 117 | sut.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); 118 | loadCount.Should().Be(1); 119 | lastChangeSet.Adds.Should().Be(10); 120 | 121 | reloader.OnNext(Unit.Default); 122 | sut.Items.Should().BeEquivalentTo(Enumerable.Range(1, 12)); 123 | lastChangeSet.Adds.Should().Be(2); 124 | } 125 | } 126 | 127 | [Fact] 128 | public void WithRetry() 129 | { 130 | int loadCount = 0; 131 | int failedCount = 0; 132 | 133 | Task> Loader() 134 | { 135 | loadCount++; 136 | 137 | if (loadCount < 3) 138 | { 139 | failedCount++; 140 | throw new Exception("Failed"); 141 | } 142 | return Task.FromResult(Enumerable.Range(1, 10)); 143 | } 144 | 145 | using (var sut = ChangeSetCreation.FromTask(Loader) 146 | .Retry(3) //in an enterprise app, would probably use a backoff retry strategy 147 | .AsObservableList()) 148 | { 149 | sut.Items.Should().BeEquivalentTo(Enumerable.Range(1, 10)); 150 | loadCount.Should().Be(3); 151 | failedCount.Should().Be(2); 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /DynamicData.Snippets/DynamicData.Snippets.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 7.2 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Filter/DynamicFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using DynamicData.Binding; 4 | using DynamicData.Snippets.Infrastructure; 5 | 6 | namespace DynamicData.Snippets.Filter 7 | { 8 | public class DynamicFilter: AbstractNotifyPropertyChanged, IDisposable 9 | { 10 | private readonly IDisposable _cleanUp ; 11 | private string _animalFilter; 12 | 13 | public IObservableList Filtered { get; } 14 | 15 | public DynamicFilter(IObservableList source, ISchedulerProvider schedulerProvider) 16 | { 17 | //produce an observable which creates a new predicate whenever AnimalFilter property changes 18 | var dynamicFilter = this.WhenValueChanged(@this => @this.AnimalFilter) 19 | .Throttle(TimeSpan.FromMilliseconds(250), schedulerProvider.Background) //throttle to prevent constant filtering (i.e. when users type) 20 | .Select(CreatePredicate); 21 | 22 | //Create list which automatically filters when AnimalFilter changes 23 | Filtered = source.Connect() 24 | .Filter(dynamicFilter) //dynamicfilter can accept any predicate observable (i.e. does not have to be based on a property) 25 | .AsObservableList(); 26 | 27 | _cleanUp = Filtered; 28 | } 29 | 30 | public string AnimalFilter 31 | { 32 | get => _animalFilter; 33 | set => SetAndRaise(ref _animalFilter, value); 34 | } 35 | 36 | private Func CreatePredicate(string text) 37 | { 38 | if (text == null || text.Length < 3) 39 | return animal => true; 40 | 41 | //the more fields which are filtered on the slower it takes for the filter to apply by generally I have never found checking a predicate to be particularly slow 42 | return animal => animal.Name.Contains(text) 43 | || animal.Type.Contains(text) 44 | || animal.Family.ToString().Contains(text); 45 | } 46 | 47 | public void Dispose() 48 | { 49 | _cleanUp.Dispose(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Filter/ExternalSourceFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | using DynamicData.Binding; 5 | using DynamicData.Snippets.Infrastructure; 6 | 7 | namespace DynamicData.Snippets.Filter 8 | { 9 | 10 | public class ExternalSourceFilter : AbstractNotifyPropertyChanged, IDisposable 11 | { 12 | private readonly IDisposable _cleanUp; 13 | 14 | public IObservableList Filtered { get; } 15 | 16 | public ExternalSourceFilter(IObservableList source, IObservableList families) 17 | { 18 | /* 19 | * Create list which is filtered from the result of another filter 20 | */ 21 | 22 | var familyFilter = families.Connect() 23 | .ToCollection() 24 | .Select(items => 25 | { 26 | bool Predicate(Animal animal) => items.Contains(animal.Family); 27 | return (Func) Predicate; 28 | }); 29 | 30 | Filtered = source.Connect() 31 | .Filter(familyFilter) 32 | .AsObservableList(); 33 | 34 | _cleanUp = Filtered; 35 | } 36 | 37 | public void Dispose() 38 | { 39 | _cleanUp.Dispose(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Filter/FilterFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using DynamicData.Snippets.Infrastructure; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace DynamicData.Snippets.Filter 7 | { 8 | public class FilterFixture 9 | { 10 | private readonly Animal[] _items = 11 | { 12 | new Animal("Holly", "Cat", AnimalFamily.Mammal), 13 | new Animal("Rover", "Dog", AnimalFamily.Mammal), 14 | new Animal("Rex", "Dog", AnimalFamily.Mammal), 15 | new Animal("Whiskers", "Cat", AnimalFamily.Mammal), 16 | new Animal("Nemo", "Fish", AnimalFamily.Fish), 17 | new Animal("Moby Dick", "Whale", AnimalFamily.Mammal), 18 | new Animal("Fred", "Frog", AnimalFamily.Amphibian), 19 | new Animal("Isaac", "Next", AnimalFamily.Amphibian), 20 | new Animal("Sam", "Snake", AnimalFamily.Reptile), 21 | new Animal("Sharon", "Red Backed Shrike", AnimalFamily.Bird), 22 | }; 23 | 24 | [Fact] 25 | public void StaticFilter() 26 | { 27 | using (var sourceList = new SourceList()) 28 | using (var sut = new StaticFilter(sourceList)) 29 | { 30 | sourceList.AddRange(_items); 31 | 32 | sut.Mammals.Items.ShouldAllBeEquivalentTo(_items.Where(a=>a.Family == AnimalFamily.Mammal)); 33 | sut.Mammals.Count.Should().Be(5); 34 | 35 | //add a new mammal to show it is included in the result set 36 | sourceList.Add(new Animal("Bob","Human",AnimalFamily.Mammal)); 37 | sut.Mammals.Count.Should().Be(6); 38 | 39 | //remove the first 4 items which will leave 2 mammals 40 | sourceList.RemoveRange(0,4); 41 | sut.Mammals.Count.Should().Be(2); 42 | } 43 | } 44 | 45 | [Fact] 46 | public void DynamicFilter() 47 | { 48 | var schedulerProvider = new TestSchedulerProvider(); 49 | using (var sourceList = new SourceList()) 50 | using (var sut = new DynamicFilter(sourceList, schedulerProvider)) 51 | { 52 | //start the scheduler 53 | schedulerProvider.TestScheduler.Start(); 54 | 55 | //add items 56 | sourceList.AddRange(_items); 57 | 58 | sut.Filtered.Items.ShouldAllBeEquivalentTo(_items); 59 | sut.Filtered.Count.Should().Be(_items.Length); 60 | 61 | //set a filter 62 | sut.AnimalFilter = "Dog"; 63 | schedulerProvider.TestScheduler.Start(); 64 | 65 | sut.Filtered.Items.ShouldAllBeEquivalentTo(_items.Where(a=>a.Type == "Dog")); 66 | sut.Filtered.Count.Should().Be(2); 67 | 68 | //add a new dog to show it is included in the result set 69 | sourceList.Add(new Animal("George", "Dog", AnimalFamily.Mammal)); 70 | sut.Filtered.Count.Should().Be(3); 71 | 72 | //add a new bird to show it is included in the result set 73 | sourceList.Add(new Animal("Peter", "Parrot", AnimalFamily.Bird)); 74 | sut.Filtered.Count.Should().Be(3); 75 | 76 | //My additions... 77 | sut.AnimalFilter = "Frog"; 78 | schedulerProvider.TestScheduler.Start(); 79 | sut.Filtered.Items.ShouldAllBeEquivalentTo(_items.Where(a => a.Type == "Frog")); 80 | sut.Filtered.Count.Should().Be(1); 81 | } 82 | } 83 | 84 | [Fact] 85 | public void PropertyFilter() 86 | { 87 | var schedulerProvider = new TestSchedulerProvider(); 88 | using (var sourceList = new SourceList()) 89 | using (var sut = new PropertyFilter(sourceList, schedulerProvider)) 90 | { 91 | //start the scheduler 92 | schedulerProvider.TestScheduler.Start(); 93 | 94 | //add items 95 | sourceList.AddRange(_items); 96 | sut.Filtered.Count.Should().Be(0); 97 | 98 | //set to true to include in the result set 99 | _items[1].IncludeInResults = true; 100 | _items[2].IncludeInResults = true; 101 | _items[3].IncludeInResults = true; 102 | 103 | //progress scheduler 104 | schedulerProvider.TestScheduler.Start(); 105 | 106 | sut.Filtered.Items.ShouldAllBeEquivalentTo(new []{ _items[1] , _items[2] , _items[3] }); 107 | } 108 | } 109 | 110 | [Fact] 111 | public void ExternalSourceFilter() 112 | { 113 | using (var sourceList = new SourceList()) 114 | using (var families = new SourceList()) 115 | using (var sut = new ExternalSourceFilter(sourceList, families)) 116 | { 117 | //add items to source 118 | sourceList.AddRange(_items); 119 | sut.Filtered.Count.Should().Be(0); 120 | 121 | families.AddRange(new []{ AnimalFamily.Amphibian, AnimalFamily.Bird }); 122 | 123 | sut.Filtered.Items.ShouldAllBeEquivalentTo(_items.Where(a => a.Family == AnimalFamily.Amphibian || a.Family==AnimalFamily.Bird)); 124 | 125 | families.Remove(AnimalFamily.Amphibian); 126 | sut.Filtered.Items.ShouldAllBeEquivalentTo(_items.Where(a => a.Family == AnimalFamily.Bird)); 127 | 128 | } 129 | } 130 | 131 | 132 | 133 | 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Filter/PropertyFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DynamicData.Binding; 3 | using DynamicData.Snippets.Infrastructure; 4 | 5 | namespace DynamicData.Snippets.Filter 6 | { 7 | public class PropertyFilter : AbstractNotifyPropertyChanged, IDisposable 8 | { 9 | private readonly IDisposable _cleanUp; 10 | 11 | public IObservableList Filtered { get; } 12 | 13 | public PropertyFilter(IObservableList source, ISchedulerProvider schedulerProvider) 14 | { 15 | /* 16 | * Create list which automatically filters: 17 | * 18 | * a) When the underlying list changes 19 | * b) When IncludeInResults property changes 20 | * c) NB: Add throttle when IncludeInResults properties can change in multiple animals in quick sucession 21 | * (i.e. each time the prop changes the filter is re-assessed potentially leading to a flurry of updates - better to slow that down) 22 | */ 23 | 24 | Filtered = source.Connect() 25 | .FilterOnProperty(animal => animal.IncludeInResults, animal => animal.IncludeInResults, TimeSpan.FromMilliseconds(250), schedulerProvider.Background) 26 | .AsObservableList(); 27 | 28 | _cleanUp = Filtered; 29 | } 30 | 31 | public void Dispose() 32 | { 33 | _cleanUp.Dispose(); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/Filter/StaticFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DynamicData.Snippets.Infrastructure; 3 | 4 | namespace DynamicData.Snippets.Filter 5 | { 6 | public class StaticFilter: IDisposable 7 | { 8 | private readonly IDisposable _cleanUp; 9 | 10 | public IObservableList Mammals { get; } 11 | 12 | public StaticFilter(IObservableList source) 13 | { 14 | //this list will automatically filter by Mammals only when the underlying list receives adds, or removes 15 | Mammals = source.Connect() 16 | .Filter(animal => animal.Family == AnimalFamily.Mammal) 17 | .AsObservableList(); 18 | 19 | _cleanUp = Mammals; 20 | } 21 | 22 | public void Dispose() 23 | { 24 | _cleanUp.Dispose(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/Group/CustomTotalRows.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | 5 | namespace DynamicData.Snippets.Group 6 | { 7 | public class CustomTotalRows: IDisposable 8 | { 9 | private readonly IDisposable _cleanUp; 10 | 11 | public IObservableCache AggregatedData { get; } 12 | 13 | public CustomTotalRows(IObservableCache source) 14 | { 15 | /* 16 | This technique can be used to create grid data with dynamic running totals 17 | 18 | [In production systems I also include the ability to have total row expanders - perhaps an example could follow another time] 19 | */ 20 | 21 | //1. create a trade proxy which enriches the trade with an aggregation id 22 | var tickers = source.Connect() 23 | .ChangeKey(proxy => new AggregationKey(AggregationType.Item, proxy.Id.ToString())) 24 | .Transform((trade, key) => new TradeProxy(trade, key)); 25 | 26 | 27 | //2. create grouping based on each ticker 28 | var tickerTotals = source.Connect() 29 | .GroupWithImmutableState(trade => new AggregationKey(AggregationType.SubTotal, trade.Ticker)) 30 | .Transform(grouping => new TradeProxy(grouping.Items.ToArray(), grouping.Key)); 31 | 32 | //3. create grouping of 1 so we can create grand total row 33 | var overallTotal = source.Connect() 34 | .GroupWithImmutableState(trade => new AggregationKey(AggregationType.GrandTotal, "All")) 35 | .Transform(grouping => new TradeProxy(grouping.Items.ToArray(), grouping.Key)); 36 | 37 | //4. join all togther so results are in a single cache 38 | AggregatedData = tickers.Or(overallTotal) 39 | .Or(tickerTotals) 40 | .AsObservableCache(); 41 | 42 | _cleanUp = AggregatedData; 43 | 44 | } 45 | 46 | public void Dispose() 47 | { 48 | _cleanUp.Dispose(); 49 | } 50 | } 51 | 52 | [DebuggerDisplay("{Ticker} ({Key.Type}) Name={Value}")] 53 | public class TradeProxy 54 | { 55 | public AggregationKey Key { get; } 56 | public string Ticker { get; } 57 | public decimal Value { get; } 58 | 59 | public TradeProxy(Trade trade, AggregationKey key) 60 | { 61 | Key = key; 62 | Ticker = key.Value; 63 | Value = trade.Value; 64 | } 65 | 66 | public TradeProxy(Trade[] trades, AggregationKey key) 67 | { 68 | Key = key; 69 | Value = trades.Select(t => t.Value).Sum(); 70 | Ticker = key.Value; 71 | } 72 | 73 | } 74 | 75 | public struct AggregationKey 76 | { 77 | public AggregationType Type { get; } 78 | public string Value { get; } 79 | 80 | public AggregationKey(AggregationType type, string value) 81 | { 82 | Type = type; 83 | Value = value; 84 | } 85 | 86 | #region Equality 87 | 88 | public bool Equals(AggregationKey other) 89 | { 90 | return Type == other.Type && string.Equals(Value, other.Value); 91 | } 92 | 93 | public override bool Equals(object obj) 94 | { 95 | if (ReferenceEquals(null, obj)) return false; 96 | return obj is AggregationKey && Equals((AggregationKey) obj); 97 | } 98 | 99 | public override int GetHashCode() 100 | { 101 | unchecked 102 | { 103 | return ((int) Type * 397) ^ (Value != null ? Value.GetHashCode() : 0); 104 | } 105 | } 106 | 107 | #endregion 108 | 109 | public override string ToString() 110 | { 111 | return $"{Type} ({Value})"; 112 | } 113 | } 114 | 115 | public enum AggregationType 116 | { 117 | Item, 118 | SubTotal, 119 | GrandTotal 120 | } 121 | 122 | public class Trade 123 | { 124 | public int Id { get; } 125 | public string Ticker { get; } 126 | public decimal Value { get; } 127 | 128 | 129 | public Trade(int id, string ticker, decimal value) 130 | { 131 | Id = id; 132 | Ticker = ticker; 133 | Value = value; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Group/GroupAndMonitorPropertyChanges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | using DynamicData.Binding; 7 | 8 | namespace DynamicData.Snippets.Group 9 | { 10 | public class GroupAndMonitorPropertyChanges: IDisposable 11 | { 12 | public IObservableList SpeciesByLetter { get; } 13 | 14 | private readonly IDisposable _cleanUp; 15 | 16 | public GroupAndMonitorPropertyChanges(ISourceList sourceList) 17 | { 18 | var shared = sourceList.Connect().Publish(); 19 | 20 | //fired when the name changes on any item in the source collection 21 | var nameChanged = shared.WhenValueChanged(species => species.Name); 22 | 23 | //group by first letter and optionally pass in the nameChanged observable (as a unit) to instruct the grouping to re-apply the grouping 24 | SpeciesByLetter = shared 25 | .GroupWithImmutableState(x => x.Name[0], nameChanged.Select(_=> Unit.Default)) 26 | .Transform(grouping => new SpeciesGroup(grouping.Key, grouping.Items)) 27 | .AsObservableList(); 28 | 29 | //*** Herein if the name of any of the species change the derived list will self maintain 30 | 31 | //Nothing happens until a Published source is connect 32 | var connected = shared.Connect(); 33 | 34 | _cleanUp = new CompositeDisposable(sourceList, SpeciesByLetter, connected); 35 | } 36 | 37 | public void Dispose() 38 | { 39 | _cleanUp.Dispose(); 40 | } 41 | } 42 | 43 | public class SpeciesGroup 44 | { 45 | public SpeciesGroup(char key, IEnumerable items) 46 | { 47 | this.Key = key; 48 | this.Items = items; 49 | } 50 | 51 | public IEnumerable Items { get; } 52 | 53 | public char Key { get; } 54 | } 55 | 56 | public class Species : AbstractNotifyPropertyChanged 57 | { 58 | public Species(string name) 59 | { 60 | Name = name; 61 | } 62 | 63 | private string _name; 64 | public string Name 65 | { 66 | get => _name; 67 | set => SetAndRaise(ref _name, value); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Group/GroupByWeek.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.Text; 6 | using DynamicData.Kernel; 7 | 8 | namespace DynamicData.Snippets.Group 9 | { 10 | public class MyCalendarEntry 11 | { 12 | public int Id { get; } 13 | public DateTime DateTime { get; } 14 | public string Entry { get; } 15 | 16 | public MyCalendarEntry(int id, DateTime dateTime, string entry) 17 | { 18 | Id = id; 19 | DateTime = dateTime; 20 | Entry = entry; 21 | } 22 | } 23 | 24 | public class MyYearAndWeekAggregations: IDisposable 25 | { 26 | private readonly IDisposable _cleanUp; 27 | public ReadOnlyObservableCollection Years { get; } 28 | 29 | public MyYearAndWeekAggregations(IObservableCache source) 30 | { 31 | _cleanUp = source.Connect() 32 | .Group(x => x.DateTime.Year) 33 | .Transform(group => new AnnualGrouping(group)) 34 | .DisposeMany() 35 | .Bind(out var years) 36 | .Subscribe(); 37 | 38 | Years = years; 39 | } 40 | public void Dispose() 41 | { 42 | _cleanUp.Dispose(); 43 | } 44 | } 45 | 46 | public class AnnualGrouping: IDisposable 47 | { 48 | private readonly IDisposable _cleanUp; 49 | 50 | public int Year { get; } 51 | public ReadOnlyObservableCollection Weeks { get; } 52 | 53 | public AnnualGrouping(IGroup annualGroup) 54 | { 55 | _cleanUp = annualGroup.Cache.Connect() 56 | .GroupWithImmutableState(x => x.DateTime.GetIso8601WeekOfYear()) 57 | .Transform(g => new WeeklyGrouping(annualGroup.Key, g.Key, g.Items)) 58 | .Bind(out var weeks) 59 | .Subscribe(); 60 | 61 | Year = annualGroup.Key; 62 | Weeks = weeks; 63 | } 64 | 65 | public void Dispose() 66 | { 67 | _cleanUp.Dispose(); 68 | } 69 | } 70 | 71 | public class WeeklyGrouping 72 | { 73 | public int Year { get; } 74 | public int Week { get; } 75 | private List Entries { get; } 76 | 77 | public WeeklyGrouping(int year, int week, IEnumerable entries) 78 | { 79 | Year = year; 80 | Week = week; 81 | Entries = new List(entries); 82 | } 83 | } 84 | 85 | public static class MySuperCoolExtensions 86 | { 87 | public static int GetIso8601WeekOfYear(this DateTime time) 88 | { 89 | DayOfWeek day = CultureInfo.InvariantCulture.Calendar.GetDayOfWeek(time); 90 | if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday) 91 | { 92 | time = time.AddDays(3); 93 | } 94 | return CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(time, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Group/GroupFixture.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolandPheasant/DynamicData.Snippets/e294b28247e41a032105828b6ca663ca16104690/DynamicData.Snippets/Group/GroupFixture.cs -------------------------------------------------------------------------------- /DynamicData.Snippets/Group/XamarinFormsGrouping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | using DynamicData.Binding; 8 | using DynamicData.Snippets.Infrastructure; 9 | 10 | namespace DynamicData.Snippets.Group 11 | { 12 | public sealed class XamarinFormsGrouping: AbstractNotifyPropertyChanged, IDisposable 13 | { 14 | private readonly IDisposable _cleanUp; 15 | 16 | public ReadOnlyObservableCollection FamilyGroups { get; } 17 | 18 | public XamarinFormsGrouping(IObservableList source, ISchedulerProvider schedulerProvider) 19 | { 20 | /* Xamarin forms is a bit dumb and cannot handle nested observable collections. 21 | * To cirumvent this limitation, create a specialist observable collection with headers and use dynamic data to manage it */ 22 | 23 | //create an observable predicate 24 | var observablePredicate = this.WhenValueChanged(@this => @this.Filter).ObserveOn(schedulerProvider.Background); 25 | 26 | _cleanUp = source.Connect() 27 | .Filter(observablePredicate) //Apply filter dynamically 28 | .GroupOn(arg => arg.Family) //create a dynamic group 29 | .Transform(grouping => new AnimalGroup(grouping, schedulerProvider)) //transform into a specialised observable collection 30 | .Sort(SortExpressionComparer.Ascending(a => a.Family)) 31 | .ObserveOn(schedulerProvider.MainThread) 32 | .Bind(out var animals) 33 | .DisposeMany() //use DisposeMany() because the grouping is disposable 34 | .Subscribe(); 35 | 36 | FamilyGroups = animals; 37 | } 38 | 39 | private Func _filter = a => true; 40 | public Func Filter 41 | { 42 | get => _filter; 43 | set => SetAndRaise(ref _filter, value); 44 | } 45 | 46 | public void Dispose() 47 | { 48 | _cleanUp.Dispose(); 49 | } 50 | } 51 | 52 | [DebuggerDisplay("{Header}")] 53 | public sealed class AnimalGroup : ObservableCollectionExtended, IDisposable 54 | { 55 | private readonly IDisposable _cleanUp; 56 | 57 | public AnimalFamily Family { get; } 58 | 59 | public AnimalGroup(IGroup grouping, ISchedulerProvider schedulerProvider) 60 | { 61 | this.Family = grouping.GroupKey; 62 | 63 | //load and sort the grouped list 64 | var dataLoader = grouping.List.Connect() 65 | .Sort(SortExpressionComparer.Ascending(a => a.Name).ThenByAscending(a => a.Type)) 66 | .ObserveOn(schedulerProvider.MainThread) 67 | .Bind(this, 2000) //make the reset threshold large because xamarin is slow when reset is called (or at least I think it is @erlend, please enlighten me ) 68 | .Subscribe(); 69 | 70 | //set the header when the group coount changes 71 | var headerSetter = grouping.List.CountChanged 72 | .Select(count => $"{grouping.GroupKey} - {count} items(s)") 73 | .ObserveOn(schedulerProvider.MainThread) 74 | .Subscribe(text => Header = text); 75 | 76 | _cleanUp = new CompositeDisposable(dataLoader, headerSetter); 77 | } 78 | 79 | string _header; 80 | public string Header 81 | { 82 | get => _header; 83 | set 84 | { 85 | _header = value; 86 | OnPropertyChanged(new PropertyChangedEventArgs(nameof(Header))); 87 | } 88 | } 89 | 90 | public void Dispose() 91 | { 92 | _cleanUp.Dispose(); 93 | } 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/Infrastructure/Animal.cs: -------------------------------------------------------------------------------- 1 | using DynamicData.Binding; 2 | 3 | namespace DynamicData.Snippets.Infrastructure 4 | { 5 | public enum AnimalFamily 6 | { 7 | Mammal, 8 | Reptile, 9 | Fish, 10 | Amphibian, 11 | Bird 12 | } 13 | 14 | public class Animal: AbstractNotifyPropertyChanged 15 | { 16 | public string Name { get; } 17 | public string Type { get; } 18 | public AnimalFamily Family { get; } 19 | 20 | private bool _includeInResults; 21 | public bool IncludeInResults 22 | { 23 | get => _includeInResults; 24 | set => SetAndRaise(ref _includeInResults, value); 25 | } 26 | 27 | public Animal(string name, string type, AnimalFamily family) 28 | { 29 | Name = name; 30 | Type = type; 31 | Family = family; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Infrastructure/DynamicDataEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DynamicData.Aggregation; 3 | 4 | namespace DynamicData.Snippets.Infrastructure 5 | { 6 | public static class DynamicDataEx 7 | { 8 | public static IObservable> ExcludeSameReferenceUpdates(this IObservable> source) 9 | { 10 | return source.IgnoreUpdateWhen((current, previous) => ReferenceEquals(current, previous)); 11 | } 12 | 13 | public static IObservable Count(this IObservable> source) 14 | { 15 | return source.ForAggregation().Count(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/Infrastructure/ISchedulerProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Concurrency; 2 | 3 | namespace DynamicData.Snippets.Infrastructure 4 | { 5 | public interface ISchedulerProvider 6 | { 7 | IScheduler MainThread { get; } 8 | IScheduler Background { get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Infrastructure/ObservableEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive; 3 | using System.Reactive.Linq; 4 | 5 | namespace DynamicData.Snippets.Infrastructure 6 | { 7 | public static class ObservableEx 8 | { 9 | public static IObservable ToUnit(this IObservable source) 10 | { 11 | return source.Select(_ => Unit.Default); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Infrastructure/StringEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace System 5 | { 6 | public static class StringEx 7 | { 8 | public static bool Contains(this string source, string toCheck, StringComparison comparison) 9 | { 10 | return source.IndexOf(toCheck, comparison) >= 0; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Infrastructure/TestSchedulerProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Concurrency; 2 | using Microsoft.Reactive.Testing; 3 | 4 | namespace DynamicData.Snippets.Infrastructure 5 | { 6 | public class TestSchedulerProvider : ISchedulerProvider 7 | { 8 | public TestScheduler TestScheduler { get; } = new TestScheduler(); 9 | 10 | private readonly IScheduler _mainThread; 11 | private readonly IScheduler _background; 12 | 13 | IScheduler ISchedulerProvider.MainThread => _mainThread; 14 | IScheduler ISchedulerProvider.Background => _background; 15 | 16 | public TestSchedulerProvider() 17 | { 18 | _mainThread = Scheduler.Immediate; 19 | _background = TestScheduler; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/InspectItems/InspectCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | 5 | namespace DynamicData.Snippets.InspectItems 6 | { 7 | public class InspectCollection : IDisposable 8 | { 9 | private readonly IDisposable _cleanUp; 10 | 11 | public InspectCollection(ISourceList source) 12 | { 13 | /* 14 | Example to illustrate how to inspect an entire collection when items are added or removed 15 | */ 16 | 17 | _cleanUp = source.Connect() 18 | .ToCollection() 19 | .Select(items => 20 | { 21 | return new 22 | { 23 | DistinctCount = items.Select(x => x.Value).Distinct().Count(), 24 | Count = items.Count 25 | }; 26 | } 27 | ) 28 | .Subscribe(x => 29 | { 30 | DistinctCount = x.DistinctCount; 31 | Count = x.Count; 32 | }); 33 | } 34 | 35 | public int DistinctCount { get; set; } 36 | public int Count { get; set; } 37 | 38 | public void Dispose() 39 | { 40 | _cleanUp.Dispose(); 41 | } 42 | } 43 | 44 | public class SimpleImmutableObject 45 | { 46 | public int Id { get; } 47 | public string Value { get; } 48 | 49 | public SimpleImmutableObject(int id, string value) 50 | { 51 | Id = id; 52 | Value = value; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DynamicData.Snippets/InspectItems/InspectCollectionWithObservable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | using System.Reactive.Subjects; 7 | using DynamicData.Snippets.Infrastructure; 8 | 9 | namespace DynamicData.Snippets.InspectItems 10 | { 11 | public class InspectCollectionWithObservable : IDisposable 12 | { 13 | private readonly IDisposable _cleanUp; 14 | 15 | public InspectCollectionWithObservable(ISourceList source) 16 | { 17 | /* 18 | Example to illustrate how to inspect an entire collection and collate observable state. 19 | */ 20 | 21 | //Capture the state of IsActive observable notification for each item in the collection 22 | var observableWithState = source.Connect().Transform(obj => new ObservableState(obj)).Publish(); 23 | 24 | //fires an observable when any of the inner observables change 25 | var activeChanged = observableWithState.MergeMany(state => state.IsActive) 26 | .ToUnit() 27 | .StartWith(Unit.Default); //start with unit to ensure combine latest (below) yields when collection is loaded 28 | 29 | //Reveal the entire collecton when the underlying observable list changes i.e. adds, removes and replaces 30 | var collectionChanged = observableWithState.ToCollection(); 31 | 32 | //combine latest collection and observable notifications and produce result indicating whether all items are Active 33 | var areAllActive = collectionChanged.CombineLatest(activeChanged, (items, _) => 34 | { 35 | return items.Any() && items.All(state => state.LatestValue.HasValue && state.LatestValue == true); 36 | }); 37 | 38 | _cleanUp = new CompositeDisposable(areAllActive.Subscribe(allActive => AllActive = allActive), 39 | observableWithState.Connect()); 40 | } 41 | 42 | public bool AllActive { get; set; } 43 | 44 | private class ObservableState 45 | { 46 | public bool? LatestValue { get; private set; } 47 | public IObservable IsActive { get; } 48 | 49 | public ObservableState(SimpleObjectWithObservable source) 50 | { 51 | IsActive = source.IsActive.Do(value => LatestValue = value); 52 | } 53 | } 54 | 55 | public void Dispose() 56 | { 57 | _cleanUp.Dispose(); 58 | } 59 | } 60 | 61 | public class SimpleObjectWithObservable 62 | { 63 | public int Id { get; } 64 | 65 | private readonly ISubject _isActiveSubject = new Subject(); 66 | 67 | public SimpleObjectWithObservable(int id) 68 | { 69 | Id = id; 70 | } 71 | 72 | public IObservable IsActive => _isActiveSubject; 73 | 74 | public void SetIsActive(bool isActive) 75 | { 76 | _isActiveSubject.OnNext(isActive); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /DynamicData.Snippets/InspectItems/InspectCollectionWithPropertyChanges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | using DynamicData.Binding; 5 | 6 | namespace DynamicData.Snippets.InspectItems 7 | { 8 | public class InspectCollectionWithPropertyChanges: IDisposable 9 | { 10 | private readonly IDisposable _cleanUp; 11 | 12 | public InspectCollectionWithPropertyChanges(ISourceList source) 13 | { 14 | /* 15 | Example to illustrate how to inspect an entire collection when properties change. 16 | */ 17 | 18 | //refresh entire collection when properties change 19 | _cleanUp = source.Connect() 20 | .AutoRefresh(vm => vm.IsActive) 21 | .ToCollection() 22 | .Select(items => 23 | { 24 | //produce a new result when the collection itself changes, or when IsActive changes 25 | //(any result can be returned) 26 | return new 27 | { 28 | AllActive = items.All(i => i.IsActive), 29 | AllInActive = items.All(i => !i.IsActive), 30 | AnyActive = items.Any(i => i.IsActive), 31 | Count = items.Count, 32 | }; 33 | }).Subscribe(x => 34 | { 35 | AllActive = x.AllActive; 36 | AllInActive = x.AllInActive; 37 | AnyActive = x.AnyActive; 38 | Count = x.Count; 39 | }); 40 | } 41 | 42 | public bool AllActive { get; set; } 43 | public bool AllInActive { get; set; } 44 | public bool AnyActive { get; set; } 45 | public int Count { get; set; } 46 | 47 | public void Dispose() 48 | { 49 | _cleanUp.Dispose(); 50 | } 51 | } 52 | 53 | public class SimpleNotifyPropertyChangedObject : AbstractNotifyPropertyChanged 54 | { 55 | public int Id { get; } 56 | 57 | public SimpleNotifyPropertyChangedObject(int id) 58 | { 59 | Id = id; 60 | } 61 | 62 | private bool _isActive; 63 | 64 | public bool IsActive 65 | { 66 | get => _isActive; 67 | set => SetAndRaise(ref _isActive, value); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /DynamicData.Snippets/InspectItems/InspectItemsFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace DynamicData.Snippets.InspectItems 6 | { 7 | 8 | public class InspectItemsFixture 9 | { 10 | [Fact] 11 | public void InspectCollection() 12 | { 13 | var items = new[] {"A", "A", "B", "C"} 14 | .Select((str, index) => new SimpleImmutableObject(index, str)) 15 | .ToArray(); 16 | 17 | using (var sourceList = new SourceList()) 18 | using (var sut = new InspectCollection(sourceList)) 19 | { 20 | sourceList.AddRange(items); 21 | 22 | sut.DistinctCount.Should().Be(3); 23 | sut.Count.Should().Be(4); 24 | 25 | sourceList.RemoveAt(0); 26 | 27 | sut.DistinctCount.Should().Be(3); 28 | sut.Count.Should().Be(3); 29 | 30 | } 31 | } 32 | 33 | [Fact] 34 | public void InspectCollectionWithPropertyChanges() 35 | { 36 | var initialItems = Enumerable.Range(1, 10) 37 | .Select(i => new SimpleNotifyPropertyChangedObject(i)) 38 | .ToArray(); 39 | 40 | using (var sourceList = new SourceList()) 41 | using (var sut = new InspectCollectionWithPropertyChanges(sourceList)) 42 | { 43 | sourceList.AddRange(initialItems); 44 | 45 | //check values are set when collection is loaded 46 | sut.AllActive.Should().Be(false); 47 | sut.AllInActive.Should().Be(true); 48 | sut.Count.Should().Be(10); 49 | 50 | //change some properties and check aggregated values 51 | initialItems[1].IsActive = true; 52 | sut.AllInActive.Should().Be(false); 53 | 54 | foreach (var item in initialItems) 55 | item.IsActive = true; 56 | 57 | sut.AllActive.Should().Be(true); 58 | sut.AllInActive.Should().Be(false); 59 | 60 | // change the underlying collection 61 | sourceList.RemoveRange(0, 5); 62 | 63 | sut.AllActive.Should().Be(true); 64 | sut.AllInActive.Should().Be(false); 65 | sut.Count.Should().Be(5); 66 | 67 | } 68 | } 69 | 70 | [Fact] 71 | public void InspectCollectionWithObservable() 72 | { 73 | var initialItems = Enumerable.Range(1, 10) 74 | .Select(i => new SimpleObjectWithObservable(i)) 75 | .ToArray(); 76 | 77 | //result should only be true when all items are set to true 78 | using (var sourceList = new SourceList()) 79 | using (var sut = new InspectCollectionWithObservable(sourceList)) 80 | { 81 | 82 | sourceList.AddRange(initialItems); 83 | sut.AllActive.Should().Be(false); 84 | 85 | //should remain false because 86 | initialItems[0].SetIsActive(true); 87 | sut.AllActive.Should().Be(false); 88 | 89 | //set all items to true 90 | foreach (var item in initialItems) item.SetIsActive(true); 91 | sut.AllActive.Should().Be(true); 92 | 93 | initialItems[0].SetIsActive(false); 94 | sut.AllActive.Should().Be(false); 95 | 96 | sourceList.Clear(); 97 | sut.AllActive.Should().Be(false); 98 | } 99 | } 100 | 101 | [Theory] 102 | [InlineData(MonitorSelectedItemsMode.UsingEntireCollection)] 103 | [InlineData(MonitorSelectedItemsMode.UsingFilterOnProperty)] 104 | public void MonitorSelectedItems(MonitorSelectedItemsMode mode) 105 | { 106 | var initialItems = Enumerable.Range(1, 10) 107 | .Select(i => new SelectableItem(i)) 108 | .ToArray(); 109 | 110 | //result should only be true when all items are set to true 111 | using (var sourceList = new SourceList()) 112 | using (var sut = new MonitorSelectedItems(sourceList, mode)) 113 | { 114 | sourceList.AddRange(initialItems); 115 | sut.HasSelection.Should().Be(false); 116 | sut.SelectedMessage.Should().Be("Nothing Selected"); 117 | 118 | initialItems[0].IsSelected = true; 119 | sut.HasSelection.Should().Be(true); 120 | sut.SelectedMessage.Should().Be("1 item selected"); 121 | 122 | initialItems[1].IsSelected = true; 123 | sut.HasSelection.Should().Be(true); 124 | sut.SelectedMessage.Should().Be("2 items selected"); 125 | 126 | //remove the selected items 127 | sourceList.RemoveRange(0,2); 128 | sut.HasSelection.Should().Be(false); 129 | sut.SelectedMessage.Should().Be("Nothing Selected"); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /DynamicData.Snippets/InspectItems/KeepItemSelected.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using DynamicData.Binding; 4 | 5 | namespace DynamicData.Snippets.InspectItems 6 | { 7 | public class KeepItemSelected : AbstractNotifyPropertyChanged, IDisposable 8 | { 9 | private readonly IDisposable _cleanUp; 10 | private KeepSelectedObject _selectedItem; 11 | 12 | public KeepItemSelected(IObservableCache source) 13 | { 14 | //an observable of updates to the selected item. When selecteditem == null, do nothing 15 | var selectedItemUpdates = this.WhenValueChanged(x => x.SelectedItem) 16 | .Select(selected => selected == null ? Observable.Never() : source.WatchValue(selected.Id)) 17 | .Switch(); 18 | 19 | //when the item updates, reset the selected item. 20 | _cleanUp = selectedItemUpdates.Subscribe(selected => SelectedItem = selected); 21 | } 22 | 23 | /// 24 | /// This is kept in sync with updates coming from the cache. 25 | /// 26 | public KeepSelectedObject SelectedItem 27 | { 28 | get => _selectedItem; 29 | set => SetAndRaise(ref _selectedItem, value); 30 | } 31 | 32 | public void Dispose() 33 | { 34 | _cleanUp.Dispose(); 35 | } 36 | } 37 | 38 | public class KeepSelectedObject 39 | { 40 | public int Id { get; } 41 | public string Label { get; } 42 | 43 | public KeepSelectedObject(int id, string label) 44 | { 45 | Id = id; 46 | Label = label; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/InspectItems/MonitorSelectedItems.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | using DynamicData.Binding; 7 | using DynamicData.Snippets.Infrastructure; 8 | 9 | namespace DynamicData.Snippets.InspectItems 10 | { 11 | public class MonitorSelectedItems : IDisposable 12 | { 13 | private readonly ISourceList _source; 14 | private readonly IDisposable _cleanUp; 15 | 16 | public bool HasSelection { get; set; } 17 | public string SelectedMessage { get; set; } 18 | 19 | public MonitorSelectedItems(ISourceList source, MonitorSelectedItemsMode mode) 20 | { 21 | _source = source; 22 | 23 | //both methods produce the same result. However, UsingEntireCollection() enables producing values of selected and not-selected items 24 | _cleanUp = mode == MonitorSelectedItemsMode.UsingFilterOnProperty 25 | ? UseFilterOnProperty() 26 | : UseEntireCollection(); 27 | } 28 | 29 | private IDisposable UseEntireCollection() 30 | { 31 | //produce an observable when the underlying list changes, or when IsSelected changes 32 | var shared = _source.Connect().Publish(); 33 | var selectedChanged = shared.WhenPropertyChanged(si => si.IsSelected).ToUnit().StartWith(Unit.Default); 34 | var collectionChanged = shared.ToCollection().CombineLatest(selectedChanged, (items, _) => items).Publish(); 35 | 36 | return new CompositeDisposable 37 | ( 38 | collectionChanged.Select(items => items.Any(si => si.IsSelected)).Subscribe(result => HasSelection = result), 39 | collectionChanged.Select(items => 40 | { 41 | var count = items.Count(si => si.IsSelected); 42 | if (count == 0) return "Nothing Selected"; 43 | return count == 1 ? $"{count} item selected" : $"{count} items selected"; 44 | }) 45 | .Subscribe(result => SelectedMessage = result), 46 | shared.Connect(), 47 | collectionChanged.Connect() 48 | ); 49 | } 50 | 51 | private IDisposable UseFilterOnProperty() 52 | { 53 | var selectedItems = _source.Connect() 54 | .AutoRefresh(si => si.IsSelected) 55 | .Filter(si => si.IsSelected) 56 | .ToCollection() 57 | .StartWithEmpty() 58 | .Publish(); 59 | 60 | return new CompositeDisposable 61 | ( 62 | selectedItems.Select(items => items.Any(si => si.IsSelected)).Subscribe(result => HasSelection = result), 63 | selectedItems.Select(items => 64 | { 65 | var count = items.Count(si => si.IsSelected); 66 | if (count == 0) return "Nothing Selected"; 67 | return count == 1 ? $"{count} item selected" : $"{count} items selected"; 68 | }) 69 | .Subscribe(result => SelectedMessage = result), 70 | selectedItems.Connect() 71 | ); 72 | } 73 | 74 | public void Dispose() 75 | { 76 | _cleanUp.Dispose(); 77 | } 78 | } 79 | 80 | public enum MonitorSelectedItemsMode 81 | { 82 | UsingFilterOnProperty, 83 | UsingEntireCollection 84 | } 85 | 86 | public class SelectableItem : AbstractNotifyPropertyChanged 87 | { 88 | public int Id { get; } 89 | 90 | public SelectableItem(int id) 91 | { 92 | Id = id; 93 | } 94 | 95 | private bool _isSelected; 96 | 97 | public bool IsSelected 98 | { 99 | get => _isSelected; 100 | set => SetAndRaise(ref _isSelected, value); 101 | } 102 | 103 | protected bool Equals(SelectableItem other) 104 | { 105 | return Id == other.Id; 106 | } 107 | 108 | public override bool Equals(object obj) 109 | { 110 | if (ReferenceEquals(null, obj)) return false; 111 | if (ReferenceEquals(this, obj)) return true; 112 | if (obj.GetType() != this.GetType()) return false; 113 | return Equals((SelectableItem) obj); 114 | } 115 | 116 | public override int GetHashCode() 117 | { 118 | return Id; 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/Join/JoinBasedOnListOfIds.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DynamicData.Snippets.Join 4 | { 5 | class JoinBasedOnListOfIds 6 | { 7 | public JoinBasedOnListOfIds(IObservableCache users, IObservableCache roles) 8 | { 9 | //TODO: Add overload to join many which enables joining on an array 10 | 11 | //users.Connect().JoinMany(roles.Connect(), 12 | // // select some sort of list with ids 13 | // user => user.Roles, 14 | // // select right key 15 | // role => role.Id, 16 | // // join every list of every user with the right source and produce a list of matching values 17 | // (user, roles) => new UserViewModel 18 | // { 19 | // GroupHeader = user.GroupHeader, 20 | // Roles = roles 21 | // }); 22 | 23 | } 24 | } 25 | 26 | class User 27 | { 28 | public int Id { get; set; } 29 | 30 | public string Name { get; set; } 31 | 32 | public List Roles { get; set; } 33 | } 34 | 35 | class Role 36 | { 37 | public int Id { get; set; } 38 | 39 | public string Name { get; set; } 40 | } 41 | 42 | class UserViewModel 43 | { 44 | public string Name { get; set; } 45 | 46 | public List Roles { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Paging/PagingFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Subjects; 3 | using DynamicData.Snippets.Infrastructure; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace DynamicData.Snippets.Paging 8 | { 9 | public class PagingFixture 10 | { 11 | private readonly Animal[] _items = 12 | { 13 | new Animal("Holly", "Cat", AnimalFamily.Mammal), 14 | new Animal("Rover", "Dog", AnimalFamily.Mammal), 15 | new Animal("Rex", "Dog", AnimalFamily.Mammal), 16 | new Animal("Whiskers", "Cat", AnimalFamily.Mammal), 17 | new Animal("Nemo", "Fish", AnimalFamily.Fish), 18 | new Animal("Moby Dick", "Whale", AnimalFamily.Mammal), 19 | new Animal("Fred", "Frog", AnimalFamily.Amphibian), 20 | new Animal("Isaac", "Next", AnimalFamily.Amphibian), 21 | new Animal("Sam", "Snake", AnimalFamily.Reptile), 22 | new Animal("Sharon", "Red Backed Shrike", AnimalFamily.Bird), 23 | }; 24 | 25 | [Fact] 26 | public void SimplePagging() 27 | { 28 | /* 29 | * The resulting data will match the exact page parameters specified 30 | * 31 | * 1. If you request new PageRequest(1, 5) you will get the first 5 items on page 1 32 | * 2. If the next request is new PageRequest(1, 6) you will get the first 6 items on page 1 33 | * [the second call will is clever enough to transmit a changeset with a single change as it does a diff set] 34 | * 35 | * 3. If you call new PageRequest(2,2) this changes the page to items on the new page and removes the old ones 36 | * 37 | * 4. If any changes take place to the underlaying data source, the current page will self-maintain 38 | */ 39 | 40 | 41 | using (var pager = new BehaviorSubject(new PageRequest(0, 0))) 42 | using (var sourceList = new SourceList()) 43 | using (var sut = new SimplePagging(sourceList, pager)) 44 | { 45 | // Add items to source 46 | sourceList.AddRange(_items); 47 | 48 | // No page was requested, so no data should be returned 49 | sut.Paged.Count.Should().Be(0); 50 | 51 | // Requested first 2 items from the underlying data 52 | pager.OnNext(new PageRequest(1, 2)); 53 | sut.Paged.Count.Should().Be(2); 54 | 55 | // Requested first 4 items from the underlying data -> expect a changeset of 2 56 | pager.OnNext(new PageRequest(1, 4)); 57 | sut.Paged.Count.Should().Be(4); 58 | 59 | // Requested first 4 items from page 2 the underlying data -> expect a changeset of 4 60 | pager.OnNext(new PageRequest(2, 4)); 61 | sut.Paged.Count.Should().Be(4); 62 | 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Paging/SimplePagging.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using DynamicData.Binding; 4 | using DynamicData.Snippets.Infrastructure; 5 | 6 | namespace DynamicData.Snippets.Paging 7 | { 8 | public class SimplePagging : AbstractNotifyPropertyChanged, IDisposable 9 | { 10 | private readonly IDisposable _cleanUp; 11 | 12 | public IObservableList Paged { get; } 13 | 14 | public SimplePagging(IObservableList source, IObservable pager) 15 | { 16 | Paged = source.Connect() 17 | .Page(pager) 18 | .Do(changes => Console.WriteLine(changes.TotalChanges), ex => Console.WriteLine(ex)) //added as a quick and dirty way to debug 19 | .AsObservableList(); 20 | 21 | _cleanUp = Paged; 22 | } 23 | 24 | public void Dispose() 25 | { 26 | _cleanUp.Dispose(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Sorting/ChangeComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using DynamicData.Binding; 4 | 5 | namespace DynamicData.Snippets.Sorting 6 | { 7 | public enum ChangeComparereOption 8 | { 9 | Ascending, 10 | Descending 11 | } 12 | 13 | public sealed class ChangeComparer : AbstractNotifyPropertyChanged, IDisposable 14 | { 15 | private ChangeComparereOption _option; 16 | private readonly IDisposable _cleanUp; 17 | 18 | public IObservableList DataSource { get; } 19 | 20 | public ChangeComparer(IObservableList source) 21 | { 22 | /* 23 | * Pass IObservable> into the sort operator to switch sorting 24 | * 25 | * The same concept applies to the ObservableCache 26 | */ 27 | 28 | var optionChanged = this.WhenValueChanged(@this => @this.Option) 29 | .Select(opt => opt == ChangeComparereOption.Ascending 30 | ? SortExpressionComparer.Ascending(i => i) 31 | : SortExpressionComparer.Descending(i => i)); 32 | 33 | //create a sorted observable list 34 | DataSource = source.Connect() 35 | .Sort(optionChanged) 36 | .AsObservableList(); 37 | 38 | _cleanUp = DataSource; 39 | } 40 | 41 | public ChangeComparereOption Option 42 | { 43 | get => _option; 44 | set => SetAndRaise(ref _option, value); 45 | } 46 | 47 | public void Dispose() 48 | { 49 | _cleanUp.Dispose(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Sorting/CustomBinding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using DynamicData.Binding; 4 | using DynamicData.Snippets.Infrastructure; 5 | 6 | namespace DynamicData.Snippets.Sorting 7 | { 8 | /// 9 | /// 10 | /// 11 | public class CustomBinding: IDisposable 12 | { 13 | private readonly IDisposable _cleanUp; 14 | 15 | public ReadOnlyObservableCollection Data { get; } 16 | 17 | public CustomBinding(IObservableCache source) 18 | { 19 | /* 20 | Sometimes the default binding does not behave exactly as you want. 21 | Using VariableThresholdObservableCollectionAdaptor is an example of how you can inject your own behaviour. 22 | */ 23 | 24 | Threshold = 5; 25 | 26 | _cleanUp = source.Connect() 27 | .Sort(SortExpressionComparer.Ascending(a => a.Name)) 28 | .Bind(out var data, adaptor: new VariableThresholdObservableCollectionAdaptor(() => Threshold)) 29 | .Subscribe(); 30 | 31 | Data = data; 32 | } 33 | 34 | public int Threshold { get; set; } 35 | 36 | public void Dispose() 37 | { 38 | _cleanUp.Dispose(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Sorting/SortFixture.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Collections.Specialized; 4 | using System.Linq; 5 | using DynamicData.Snippets.Infrastructure; 6 | using FluentAssertions; 7 | using Xunit; 8 | 9 | namespace DynamicData.Snippets.Sorting 10 | { 11 | 12 | public class SortFixture 13 | { 14 | private readonly Animal[] _items = new[] 15 | { 16 | new Animal("Holly", "Cat", AnimalFamily.Mammal), 17 | new Animal("Rover", "Dog", AnimalFamily.Mammal), 18 | new Animal("Rex", "Dog", AnimalFamily.Mammal), 19 | new Animal("Whiskers", "Cat", AnimalFamily.Mammal), 20 | new Animal("Nemo", "Fish", AnimalFamily.Fish), 21 | new Animal("Moby Dick", "Whale", AnimalFamily.Mammal), 22 | new Animal("Fred", "Frog", AnimalFamily.Amphibian), 23 | new Animal("Isaac", "Next", AnimalFamily.Amphibian), 24 | new Animal("Sam", "Snake", AnimalFamily.Reptile), 25 | new Animal("Sharon", "Red Backed Shrike", AnimalFamily.Bird), 26 | }; 27 | 28 | 29 | [Fact] 30 | public void CustomBinding() 31 | { 32 | // in this test we check whether the reset threshold can be dynamically controlled 33 | 34 | using (var sourceCache = new SourceCache(a => a.Name)) 35 | using (var sut = new CustomBinding(sourceCache)) 36 | { 37 | int resetCount = 0; 38 | (sut.Data as INotifyCollectionChanged).CollectionChanged += (_, e) => 39 | { 40 | if (e.Action == NotifyCollectionChangedAction.Reset) 41 | resetCount++; 42 | }; 43 | 44 | sut.Threshold = 20; 45 | sourceCache.AddOrUpdate(_items); 46 | resetCount.Should().Be(0); 47 | 48 | sut.Threshold = 5; 49 | sourceCache.AddOrUpdate(_items); 50 | resetCount.Should().Be(1); 51 | 52 | sut.Threshold = 20; 53 | sourceCache.AddOrUpdate(_items); 54 | resetCount.Should().Be(1); 55 | } 56 | 57 | } 58 | 59 | [Fact] 60 | public void ChangeComparer() 61 | { 62 | const int size = 100; 63 | var randomValues = Enumerable.Range(1, size).OrderBy(_ => Guid.NewGuid()).ToArray(); 64 | var ascending = Enumerable.Range(1, size).ToArray(); 65 | var descending = Enumerable.Range(1, size).OrderByDescending(_ => Guid.NewGuid()).ToArray(); 66 | 67 | using (var input = new SourceList()) 68 | using (var sut = new ChangeComparer(input)) 69 | { 70 | input.AddRange(randomValues); 71 | 72 | sut.DataSource.Items.ShouldAllBeEquivalentTo(ascending); 73 | 74 | sut.Option = ChangeComparereOption.Descending; 75 | sut.DataSource.Items.ShouldAllBeEquivalentTo(descending); 76 | 77 | sut.Option = ChangeComparereOption.Ascending; 78 | sut.DataSource.Items.ShouldAllBeEquivalentTo(ascending); 79 | } 80 | } 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Sorting/VariableThresholdObservableCollectionAdaptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using DynamicData.Binding; 4 | 5 | namespace DynamicData.Snippets.Sorting 6 | { 7 | /// 8 | /// This is cloned from DynamicData and has been changed so the reset threshold can be adjusted 9 | /// 10 | public class VariableThresholdObservableCollectionAdaptor : ISortedObservableCollectionAdaptor 11 | { 12 | private readonly Func _refreshThreshold; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The number of changes before a Reset event is used 18 | public VariableThresholdObservableCollectionAdaptor(Func refreshThreshold) 19 | { 20 | _refreshThreshold = refreshThreshold; 21 | } 22 | 23 | /// 24 | /// Maintains the specified collection from the changes 25 | /// 26 | /// The changes. 27 | /// The collection. 28 | public void Adapt(ISortedChangeSet changes, IObservableCollection collection) 29 | { 30 | switch (changes.SortedItems.SortReason) 31 | { 32 | case SortReason.InitialLoad: 33 | { 34 | if (changes.Count > _refreshThreshold()) 35 | { 36 | using (collection.SuspendNotifications()) 37 | { 38 | collection.Load(changes.SortedItems.Select(kv => kv.Value)); 39 | } 40 | } 41 | else 42 | { 43 | using (collection.SuspendCount()) 44 | { 45 | DoUpdate(changes, collection); 46 | } 47 | } 48 | } 49 | break; 50 | case SortReason.ComparerChanged: 51 | case SortReason.Reset: 52 | using (collection.SuspendNotifications()) 53 | { 54 | collection.Load(changes.SortedItems.Select(kv => kv.Value)); 55 | } 56 | break; 57 | 58 | case SortReason.DataChanged: 59 | if (changes.Count > _refreshThreshold()) 60 | { 61 | using (collection.SuspendNotifications()) 62 | { 63 | collection.Load(changes.SortedItems.Select(kv => kv.Value)); 64 | } 65 | } 66 | else 67 | { 68 | using (collection.SuspendCount()) 69 | { 70 | DoUpdate(changes, collection); 71 | } 72 | } 73 | break; 74 | 75 | case SortReason.Reorder: 76 | //Updates will only be moves, so apply logic 77 | using (collection.SuspendCount()) 78 | { 79 | DoUpdate(changes, collection); 80 | } 81 | break; 82 | 83 | default: 84 | throw new ArgumentOutOfRangeException(); 85 | } 86 | } 87 | 88 | private void DoUpdate(ISortedChangeSet updates, IObservableCollection list) 89 | { 90 | foreach (var update in updates) 91 | { 92 | switch (update.Reason) 93 | { 94 | case ChangeReason.Add: 95 | list.Insert(update.CurrentIndex, update.Current); 96 | break; 97 | case ChangeReason.Remove: 98 | list.RemoveAt(update.CurrentIndex); 99 | break; 100 | case ChangeReason.Moved: 101 | list.Move(update.PreviousIndex, update.CurrentIndex); 102 | break; 103 | case ChangeReason.Update: 104 | list.RemoveAt(update.PreviousIndex); 105 | list.Insert(update.CurrentIndex, update.Current); 106 | break; 107 | } 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/Switch/SwitchDataSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using DynamicData.Binding; 4 | 5 | namespace DynamicData.Snippets.Switch 6 | { 7 | public enum SwitchDataSourceOption 8 | { 9 | SourceA, 10 | SourceB 11 | } 12 | 13 | public sealed class SwitchDataSource: AbstractNotifyPropertyChanged, IDisposable 14 | { 15 | private SwitchDataSourceOption _option; 16 | private readonly IDisposable _cleanUp; 17 | 18 | public IObservableList DataSource { get; } 19 | 20 | public SwitchDataSource(IObservableList sourceA, IObservableList sourceB) 21 | { 22 | /* 23 | * Switching data acts on IObservable> or IObservable> 24 | * 25 | * The same concept applies to the ObservableCache 26 | */ 27 | 28 | DataSource = this.WhenValueChanged(@this => @this.Option) 29 | .Select(opt => opt == SwitchDataSourceOption.SourceA ? sourceA : sourceB) 30 | .Switch() //this is dynamic data overload of Switch() 31 | .AsObservableList(); 32 | 33 | _cleanUp = DataSource; 34 | } 35 | 36 | public SwitchDataSourceOption Option 37 | { 38 | get => _option; 39 | set => SetAndRaise(ref _option, value); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | _cleanUp.Dispose(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Switch/SwitchDataSourceFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace DynamicData.Snippets.Switch 8 | { 9 | 10 | public class SwitchDataSourceFixture 11 | { 12 | [Fact] 13 | public void Switch() 14 | { 15 | using (var listA = new SourceList()) 16 | using (var listB = new SourceList()) 17 | using (var sut = new SwitchDataSource(listA, listB)) 18 | { 19 | var oddNumbers = Enumerable.Range(1, 10000).Where(i => i % 2 == 1).ToArray(); 20 | var evenNumbers = Enumerable.Range(1, 10000).Where(i => i % 2 == 2).ToArray(); 21 | 22 | listA.AddRange(oddNumbers); 23 | listB.AddRange(evenNumbers); 24 | 25 | sut.DataSource.Items.ShouldAllBeEquivalentTo(oddNumbers); 26 | 27 | sut.Option = SwitchDataSourceOption.SourceB; 28 | sut.DataSource.Items.ShouldAllBeEquivalentTo(evenNumbers); 29 | 30 | sut.Option = SwitchDataSourceOption.SourceA; 31 | sut.DataSource.Items.ShouldAllBeEquivalentTo(oddNumbers); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Transform/FlattenNestedObservableCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | 6 | namespace DynamicData.Snippets.Transform 7 | { 8 | public class FlattenNestedObservableCollection: IDisposable 9 | { 10 | public IObservableCache Children { get; } 11 | 12 | public FlattenNestedObservableCollection(IObservableCache source) 13 | { 14 | /* 15 | * Create a flat cache based on a nested observable collection. 16 | * 17 | * Since a new changeset is produced each time a parent is added, I recommend applying Batch() 18 | * after TransformMany() to reduce notifications (particularly on initial load) 19 | */ 20 | Children = source.Connect() 21 | .TransformMany(parent => parent.Children, c=>c.Name) 22 | .AsObservableCache(); 23 | } 24 | 25 | 26 | public void Dispose() 27 | { 28 | Children.Dispose(); 29 | } 30 | } 31 | 32 | public class ClassWithNestedObservableCollection 33 | { 34 | public int Id { get; } 35 | public ObservableCollection Children { get; } 36 | 37 | public ClassWithNestedObservableCollection(int id, IEnumerable animals) 38 | { 39 | Id = id; 40 | Children = new ObservableCollection(animals); 41 | } 42 | } 43 | 44 | public class NestedChild 45 | { 46 | public string Name { get; } 47 | public string Value { get; } 48 | 49 | public NestedChild(string name, string value) 50 | { 51 | Name = name; 52 | Value = value; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Transform/TransformFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using DynamicData.Binding; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace DynamicData.Snippets.Transform 8 | { 9 | 10 | public class TransformFixture 11 | { 12 | [Fact] 13 | public void FlattenObservableCollection() 14 | { 15 | var children = new[] 16 | { 17 | new NestedChild("A", "ValueA"), 18 | new NestedChild("B", "ValueB"), 19 | new NestedChild("C", "ValueC"), 20 | new NestedChild("D", "ValueD"), 21 | new NestedChild("E", "ValueE"), 22 | new NestedChild("F", "ValueF") 23 | }; 24 | 25 | var parents = new[] 26 | { 27 | new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), 28 | new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), 29 | new ClassWithNestedObservableCollection(3, new[] { children[4] }) 30 | }; 31 | 32 | using (var source = new SourceCache(x => x.Id)) 33 | using (var sut = new FlattenNestedObservableCollection(source)) 34 | { 35 | source.AddOrUpdate(parents); 36 | 37 | sut.Children.Count.Should().Be(5); 38 | sut.Children.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Take(5))); 39 | 40 | //add a child to the observable collection 41 | parents[2].Children.Add(children[5]); 42 | 43 | sut.Children.Count.Should().Be(6); 44 | sut.Children.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children)); 45 | 46 | //remove a parent and check children have moved 47 | source.RemoveKey(1); 48 | sut.Children.Count.Should().Be(4); 49 | sut.Children.Items.ShouldBeEquivalentTo(parents.Skip(1).SelectMany(p => p.Children)); 50 | 51 | //add a parent and check items have been added back in 52 | source.AddOrUpdate(parents[0]); 53 | 54 | sut.Children.Count.Should().Be(6); 55 | sut.Children.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children)); 56 | } 57 | } 58 | 59 | [Fact] 60 | public void FlattenObservableCollectionWithProjectionFromObservableCache() 61 | { 62 | var children = new[] 63 | { 64 | new NestedChild("A", "ValueA"), 65 | new NestedChild("B", "ValueB"), 66 | new NestedChild("C", "ValueC"), 67 | new NestedChild("D", "ValueD"), 68 | new NestedChild("E", "ValueE"), 69 | new NestedChild("F", "ValueF") 70 | }; 71 | 72 | var parents = new[] 73 | { 74 | new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), 75 | new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), 76 | new ClassWithNestedObservableCollection(3, new[] { children[4] }) 77 | }; 78 | 79 | using (var source = new SourceCache(x => x.Id)) 80 | using (var sut = source.Connect() 81 | .AutoRefreshOnObservable(self => self.Children.ToObservableChangeSet()) 82 | .TransformMany(parent => parent.Children.Select(c => new ProjectedNestedChild(parent, c)), c => c.Child.Name) 83 | .AsObservableCache()) 84 | { 85 | source.AddOrUpdate(parents); 86 | 87 | sut.Count.Should().Be(5); 88 | sut.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Take(5).Select(c => new ProjectedNestedChild(p, c)))); 89 | 90 | //add a child to the observable collection 91 | parents[2].Children.Add(children[5]); 92 | 93 | sut.Count.Should().Be(6); 94 | sut.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Select(c => new ProjectedNestedChild(p, c)))); 95 | 96 | //remove a parent and check children have moved 97 | source.RemoveKey(1); 98 | sut.Count.Should().Be(4); 99 | sut.Items.ShouldBeEquivalentTo(parents.Skip(1).SelectMany(p => p.Children.Select(c => new ProjectedNestedChild(p, c)))); 100 | 101 | //add a parent and check items have been added back in 102 | source.AddOrUpdate(parents[0]); 103 | 104 | sut.Count.Should().Be(6); 105 | sut.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Select(c => new ProjectedNestedChild(p, c)))); 106 | } 107 | } 108 | 109 | [Fact] 110 | public void FlattenObservableCollectionWithProjectionFromObservableList() 111 | { 112 | var children = new[] 113 | { 114 | new NestedChild("A", "ValueA"), 115 | new NestedChild("B", "ValueB"), 116 | new NestedChild("C", "ValueC"), 117 | new NestedChild("D", "ValueD"), 118 | new NestedChild("E", "ValueE"), 119 | new NestedChild("F", "ValueF") 120 | }; 121 | 122 | var parents = new[] 123 | { 124 | new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), 125 | new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), 126 | new ClassWithNestedObservableCollection(3, new[] { children[4] }) 127 | }; 128 | 129 | using (var source = new SourceList()) 130 | using (var sut = source.Connect() 131 | .AutoRefreshOnObservable(self => self.Children.ToObservableChangeSet()) 132 | .TransformMany(parent => parent.Children.Select(c => new ProjectedNestedChild(parent, c)), new ProjectNestedChildEqualityComparer()) 133 | .AsObservableList()) 134 | { 135 | source.AddRange(parents); 136 | 137 | sut.Count.Should().Be(5); 138 | sut.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Take(5).Select(c => new ProjectedNestedChild(p, c)))); 139 | 140 | //add a child to the observable collection 141 | parents[2].Children.Add(children[5]); 142 | 143 | sut.Count.Should().Be(6); 144 | sut.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Select(c => new ProjectedNestedChild(p, c)))); 145 | 146 | //remove a parent and check children have moved 147 | source.Remove(parents[0]); 148 | sut.Count.Should().Be(4); 149 | sut.Items.ShouldBeEquivalentTo(parents.Skip(1).SelectMany(p => p.Children.Select(c => new ProjectedNestedChild(p, c)))); 150 | 151 | //add a parent and check items have been added back in 152 | source.Add(parents[0]); 153 | 154 | sut.Count.Should().Be(6); 155 | sut.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Select(c => new ProjectedNestedChild(p, c)))); 156 | } 157 | } 158 | 159 | private class ProjectedNestedChild 160 | { 161 | public ClassWithNestedObservableCollection Parent { get; } 162 | 163 | public NestedChild Child { get; } 164 | 165 | public ProjectedNestedChild(ClassWithNestedObservableCollection parent, NestedChild child) 166 | { 167 | Parent = parent; 168 | Child = child; 169 | } 170 | } 171 | 172 | private class ProjectNestedChildEqualityComparer : IEqualityComparer 173 | { 174 | public bool Equals(ProjectedNestedChild x, ProjectedNestedChild y) 175 | { 176 | if (x == null || y == null) 177 | return false; 178 | 179 | return x.Child.Name == y.Child.Name; 180 | } 181 | 182 | public int GetHashCode(ProjectedNestedChild obj) 183 | { 184 | return obj.Child.Name.GetHashCode(); 185 | } 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/ViewModelTesting/ViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Reactive.Disposables; 4 | using System.Reactive.Linq; 5 | using DynamicData.Binding; 6 | using DynamicData.Snippets.Infrastructure; 7 | 8 | namespace DynamicData.Snippets.ViewModelTesting 9 | { 10 | public class ViewModel: AbstractNotifyPropertyChanged, IDisposable 11 | { 12 | public ReadOnlyObservableCollection BindingData { get; } 13 | 14 | private readonly IDisposable _cleanUp; 15 | private bool _isPaused; 16 | private bool _showEmptyView; 17 | 18 | public ViewModel(IDataProvider dataProvider, ISchedulerProvider schedulerProvider) 19 | { 20 | var paused = this.WhenValueChanged(vm => vm.IsPaused); 21 | 22 | /* 23 | * NB: When ObserveOn is required, or asynchronous threads are introduced, always paramatise the threads via a scheduler provider 24 | * or some other means of achieving the same thing 25 | */ 26 | 27 | var dataLoader = dataProvider.ItemCache 28 | .Connect() 29 | .Transform(CreateItemViewModel) 30 | .BatchIf(paused, schedulerProvider.Background) //pause the observable for fun + to illustrate background testing 31 | .Sort(SortExpressionComparer.Descending(i => i.Item.Id)) 32 | .ObserveOn(schedulerProvider.MainThread) 33 | .Bind(out var bindingData) 34 | .Subscribe(); 35 | 36 | BindingData = bindingData; 37 | 38 | var counter = dataProvider.ItemCache.CountChanged.Subscribe(i => ShowEmptyView = i == 0); 39 | 40 | _cleanUp = new CompositeDisposable(dataLoader, counter); 41 | } 42 | 43 | public bool ShowEmptyView 44 | { 45 | get => _showEmptyView; 46 | set => SetAndRaise(ref _showEmptyView, value); 47 | } 48 | 49 | public bool IsPaused 50 | { 51 | get => _isPaused; 52 | set => SetAndRaise(ref _isPaused, value); 53 | } 54 | 55 | private ItemViewModel CreateItemViewModel(Item item) 56 | { 57 | return new ItemViewModel(item); 58 | } 59 | 60 | public void Dispose() 61 | { 62 | _cleanUp.Dispose(); 63 | } 64 | } 65 | 66 | public interface IDataProvider 67 | { 68 | IObservableCache ItemCache { get; } 69 | } 70 | 71 | public class Item 72 | { 73 | public int Id { get; } 74 | 75 | public Item(int id) 76 | { 77 | Id = id; 78 | } 79 | } 80 | 81 | public class ItemViewModel 82 | { 83 | public Item Item { get; } 84 | 85 | public ItemViewModel(Item item) 86 | { 87 | Item = item; 88 | } 89 | } 90 | 91 | 92 | 93 | } -------------------------------------------------------------------------------- /DynamicData.Snippets/ViewModelTesting/ViewModelFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using DynamicData.Snippets.Infrastructure; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace DynamicData.Snippets.ViewModelTesting 8 | { 9 | 10 | public class ViewModelFixture 11 | { 12 | [Fact] 13 | public void Binding() 14 | { 15 | var schedulerProvider = new TestSchedulerProvider(); 16 | using (var testData = new DataProviderStub()) 17 | using (var sut = new ViewModel(testData, schedulerProvider)) 18 | { 19 | //Act 20 | var items = Enumerable.Range(1, 10).Select(i => new Item(i)).ToArray(); 21 | testData.Data.AddOrUpdate(items); 22 | schedulerProvider.TestScheduler.Start(); //push scheduler forward 23 | 24 | //1. Check count of data 25 | sut.BindingData.Count.Should().Be(10); 26 | 27 | //2. Check Transform and Sort 28 | var expectedData = items 29 | .Select(i => new ItemViewModel(i)) 30 | .OrderByDescending(vm => vm.Item.Id); 31 | 32 | sut.BindingData.ShouldAllBeEquivalentTo(expectedData); 33 | } 34 | } 35 | 36 | [Fact] 37 | public void IsPaused() 38 | { 39 | var schedulerProvider = new TestSchedulerProvider(); 40 | using (var testData = new DataProviderStub()) 41 | using (var sut = new ViewModel(testData, schedulerProvider)) 42 | { 43 | sut.IsPaused = true; 44 | schedulerProvider.TestScheduler.Start(); //push scheduler forward 45 | 46 | //add data after pause has started 47 | testData.Data.AddOrUpdate(Enumerable.Range(1, 10).Select(i => new Item(i))); 48 | schedulerProvider.TestScheduler.AdvanceBy(1); 49 | 50 | //check no data has been pipelined 51 | sut.BindingData.Count.Should().Be(0); 52 | 53 | //turn pause off and check the updates have been pushed through 54 | sut.IsPaused = false; 55 | schedulerProvider.TestScheduler.AdvanceBy(1); 56 | sut.BindingData.Count.Should().Be(10); 57 | } 58 | } 59 | 60 | 61 | private class DataProviderStub : IDataProvider, IDisposable 62 | { 63 | //create a backend data source for our tests 64 | public ISourceCache Data { get; } = new SourceCache(i => i.Id); 65 | 66 | private readonly IObservableCache _itemCache; 67 | IObservableCache IDataProvider.ItemCache => _itemCache; 68 | 69 | public DataProviderStub() 70 | { 71 | _itemCache = Data.AsObservableCache(); 72 | } 73 | 74 | public void Dispose() 75 | { 76 | _itemCache.Dispose(); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Virtualise/PagingListWithVirtualise.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive; 4 | using System.Reactive.Linq; 5 | using System.Reactive.Subjects; 6 | using DynamicData.Binding; 7 | using DynamicData.Snippets.Infrastructure; 8 | 9 | namespace DynamicData.Snippets.Virtualise 10 | { 11 | public class PagingListWithVirtualise : AbstractNotifyPropertyChanged, IDisposable 12 | { 13 | public IObservableList Virtualised { get; } 14 | 15 | private readonly IDisposable _cleanUp; 16 | 17 | public PagingListWithVirtualise(IObservableList source, IObservable requests) 18 | { 19 | Virtualised = source.Connect() 20 | .Virtualise(requests) 21 | .AsObservableList(); 22 | 23 | _cleanUp = Virtualised; 24 | } 25 | 26 | public void Dispose() 27 | { 28 | _cleanUp.Dispose(); 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Virtualise/PagingListWithVirtualiseFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Subjects; 2 | using DynamicData.Snippets.Infrastructure; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace DynamicData.Snippets.Virtualise 7 | { 8 | public class PagingListWithVirtualiseFixture 9 | { 10 | private readonly Animal[] _items = 11 | { 12 | new Animal("Holly", "Cat", AnimalFamily.Mammal), 13 | new Animal("Rover", "Dog", AnimalFamily.Mammal), 14 | new Animal("Rex", "Dog", AnimalFamily.Mammal), 15 | new Animal("Whiskers", "Cat", AnimalFamily.Mammal), 16 | new Animal("Nemo", "Fish", AnimalFamily.Fish), 17 | new Animal("Moby Dick", "Whale", AnimalFamily.Mammal), 18 | new Animal("Fred", "Frog", AnimalFamily.Amphibian), 19 | new Animal("Isaac", "Next", AnimalFamily.Amphibian), 20 | new Animal("Sam", "Snake", AnimalFamily.Reptile), 21 | new Animal("Sharon", "Red Backed Shrike", AnimalFamily.Bird) 22 | }; 23 | 24 | [Fact] 25 | public void PagingListWithVirtualise() 26 | { 27 | using (var pager = new BehaviorSubject(new VirtualRequest(0, 0))) 28 | using (var sourceList = new SourceList()) 29 | using (var sut = new PagingListWithVirtualise(sourceList, pager)) 30 | { 31 | // Add items to source 32 | sourceList.AddRange(_items); 33 | 34 | // Requested 0 items, so no data should be returned 35 | sut.Virtualised.Count.Should().Be(0); 36 | 37 | // Requested 2 items starting at position 0, 2 items expected 38 | pager.OnNext(new VirtualRequest(0, 2)); 39 | sut.Virtualised.Count.Should().Be(2); 40 | 41 | // Requested 2 items starting at position 2, 2 items expected 42 | pager.OnNext(new VirtualRequest(2, 2)); 43 | sut.Virtualised.Count.Should().Be(2); 44 | 45 | // Requested 5 items starting at position 0, 5 items expected 46 | pager.OnNext(new VirtualRequest(0, 5)); 47 | sut.Virtualised.Count.Should().Be(5); 48 | 49 | // Requested 1 item starting at position 5, 1 items expected 50 | pager.OnNext(new VirtualRequest(5, 1)); 51 | sut.Virtualised.Count.Should().Be(1); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Watch/SelectCacheItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Reactive.Disposables; 4 | using System.Reactive.Linq; 5 | using DynamicData.Binding; 6 | using DynamicData.Snippets.Infrastructure; 7 | 8 | namespace DynamicData.Snippets.Watch 9 | { 10 | public class SelectCacheItem: AbstractNotifyPropertyChanged, IDisposable 11 | { 12 | private readonly ISchedulerProvider _schedulerProvider; 13 | private readonly IDisposable _cleanUp; 14 | private TransformedCacheItem _selectedItem; 15 | 16 | private readonly ISourceCache _sourceCache = new SourceCache(ci=>ci.Id); 17 | private readonly IObservableCache _transformedCache; 18 | private readonly SerialDisposable _autoSelector = new SerialDisposable(); 19 | 20 | public ReadOnlyObservableCollection Data { get; } 21 | 22 | public SelectCacheItem(ISchedulerProvider schedulerProvider) 23 | { 24 | /* 25 | * Example to show how to select an item after after it has been added to a cache and subsequently transformed. 26 | * 27 | * If a filter is applied before the transform this methodology will not work. In that case, alternatives would be 28 | * to: 29 | * 30 | * 1. Add .Do(changes => some custom logic) after the bind statement 31 | * 2 Add .OnItemAdded(i => SelectedItem = i) after the bind statement 32 | * 33 | * In both these options there is no need to split the source cache into a separate transformed cache 34 | */ 35 | 36 | _schedulerProvider = schedulerProvider; 37 | 38 | _transformedCache = _sourceCache.Connect() 39 | .Transform(si => new TransformedCacheItem(si)) 40 | .AsObservableCache(); 41 | 42 | var binder = _transformedCache.Connect() 43 | .ObserveOn(schedulerProvider.MainThread) 44 | .Bind(out var data) 45 | .Subscribe(); 46 | 47 | Data = data; 48 | 49 | _cleanUp = new CompositeDisposable(binder, _transformedCache, _sourceCache); 50 | } 51 | 52 | public void Load(CacheItem[] items) 53 | { 54 | _sourceCache.AddOrUpdate(items); 55 | 56 | SelectWhenLoaded(items[0].Id); 57 | } 58 | 59 | public void AddOrUpdate(CacheItem item) 60 | { 61 | _sourceCache.AddOrUpdate(item); 62 | 63 | SelectWhenLoaded(item.Id); 64 | } 65 | 66 | private void SelectWhenLoaded(string id) 67 | { 68 | //put the code onto the main thread so it happens after binding 69 | _autoSelector.Disposable = _transformedCache.Watch(id) 70 | .ObserveOn(_schedulerProvider.MainThread) 71 | .Subscribe(change => SelectedItem = change.Current); 72 | 73 | } 74 | 75 | public TransformedCacheItem SelectedItem 76 | { 77 | get => _selectedItem; 78 | set => SetAndRaise(ref _selectedItem, value); 79 | } 80 | 81 | public void Dispose() 82 | { 83 | _cleanUp.Dispose(); 84 | } 85 | } 86 | 87 | 88 | public class CacheItem 89 | { 90 | public string Id { get; } 91 | 92 | public CacheItem(string id) 93 | { 94 | Id = id; 95 | } 96 | } 97 | 98 | public class TransformedCacheItem 99 | { 100 | public string Id { get; } 101 | 102 | public TransformedCacheItem(CacheItem cacheItem) 103 | { 104 | Id = cacheItem.Id; 105 | Description = $"Transformed {Id}"; 106 | } 107 | 108 | public string Description { get; } 109 | } 110 | 111 | 112 | 113 | } 114 | -------------------------------------------------------------------------------- /DynamicData.Snippets/Watch/SelectCacheItemFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using DynamicData.Snippets.Infrastructure; 6 | using FluentAssertions; 7 | using Xunit; 8 | 9 | namespace DynamicData.Snippets.Watch 10 | { 11 | public class SelectCacheItemFixture 12 | { 13 | 14 | [Fact] 15 | public void SelectedItemsTests() 16 | { 17 | var schedulerProvider = new TestSchedulerProvider(); 18 | var items = Enumerable.Range(1, 10).Select(i => new CacheItem(i.ToString())).ToArray(); 19 | 20 | using (var sut = new SelectCacheItem(schedulerProvider)) 21 | { 22 | sut.Load(items); 23 | 24 | sut.SelectedItem.Id.Should().Be("1"); 25 | 26 | sut.AddOrUpdate(new CacheItem("11")); 27 | 28 | sut.SelectedItem.Id.Should().Be("11"); 29 | } 30 | 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Data Snippets 2 | 3 | People are always asking me for documents or explanations of dynamic data. However as an open source developer and active family man who works in a high pressure environment, the maintenance and development of my open source projects leaves me with no time to even consider documents. 4 | 5 | Thinking about it, why do people always ask for documents. We are developers and we can write and analyse code better than we can produce documents (well most of us anyway). That is why I have created this project, with the aim to: 6 | 7 | - Regard this project as a '101 Samples project' 8 | - Ensure all examples are unit tested 9 | - Respond to queries from the community by adding new examples to the project 10 | 11 | ### Support And Contact 12 | 13 | The dynamic data chat room is a sub channel the Reactive Inc slack channel. It is an invite only forum so if you want an invite send me a message @ roland_pheasant@hotmail.com. If you would like me to produce an example to help with a particular problem, feel free to contact me on slack to discuss it further. 14 | 15 | ### Links to examples 16 | All these examples have working unit tests which allows for debugging and experimentation 17 | 18 | | Topic| Link| Description| 19 | | ------------- |-------------| -----| 20 | | AutoRefresh |[AutoRefreshForPropertyChanges.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/AutoRefresh/AutoRefreshForPropertyChanges.cs) | How to force cache operators to recalulate when using mutable objects| 21 | | Aggregation |[Aggregations.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Aggregation/Aggregations.cs) | Dynamically aggregrate items in a data source | 22 | | Creation |[ChangeSetCreation.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Creation/ChangeSetCreation.cs) | Create list and cache using first class observables | 23 | | Filtering |[StaticFilter.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Filter/StaticFilter.cs) | Filter a data source using a static predicate | 24 | | |[DynamicFilter.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Filter/DynamicFilter.cs) | Create and apply an observable predicate | 25 | | |[ExternalSourceFilter.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Filter/ExternalSourceFilter.cs) | Create an observable predicate from another data source | 26 | | |[PropertyFilter.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Filter/PropertyFilter.cs) | Filter on a property which implements ```INotifyPropertyChanged``` | 27 | | Grouping|[CustomTotalRows.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Group/CustomTotalRows.cs) | Illustrate how grouping can be used for custom aggregation | 28 | | |[XamarinFormsGrouping.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Group/XamarinFormsGrouping.cs) | Bespoke grouping with Xamarin Forms | 29 | | |[GroupAndMonitorPropertyChanges.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Group/GroupAndMonitorPropertyChanges.cs) | Group on the first letter of a property and update grouping when the property changes | 30 | | Inspect Collection|[InspectCollection.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/InspectItems/InspectCollection.cs) | Produce an observable based on the contents of the datasource | 31 | | |[InspectCollectionWithPropertyChanges.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/InspectItems/InspectCollectionWithPropertyChanges.cs) | Produce an observable based on the contents of the data source, which also fires when a specified property changes | 32 | | |[InspectCollectionWithObservable.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/InspectItems/InspectCollectionWithObservable.cs) | Produce an observable based on the contents of the data source, whose values are supplied by an observable on each item in the collection| 33 | | |[MonitorSelectedItems.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/InspectItems/MonitorSelectedItems.cs) | Monitor a collection of items which have an IsSelected property and produce observables based on selection| 34 | | Sorting|[ChangeComparer.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Sorting/ChangeComparer.cs) | How to dynamically sort a collection using an observable comparer| 35 | | |[CustomBinding.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Sorting/CustomBinding.cs) | Customise binding behaviour for a sorted data source| 36 | | Switch |[SwitchDataSource.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Switch/SwitchDataSource.cs) | Toggle between different data sources| 37 | | Transform |[FlattenNestedObservableCollection.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/Transform/FlattenNestedObservableCollection.cs) | Flatten nested observable collections into an observable data source | 38 | | Testing|[ViewModel.cs](https://github.com/RolandPheasant/DynamicData.Snippets/blob/master/DynamicData.Snippets/ViewModelTesting/ViewModel.cs) | Illustrates how to test a view model when using dynamic data| 39 | 40 | ### Real world examples of Dynamic Data 41 | 42 | I have created several Dynamic Data in action projects which illustrates the usage of dynamic data. I encourage people who want to see these real world examples to take a look at the following projects to see the capabilities of Dynamic Data. 43 | 44 | Include are: 45 | 46 | [Dynamic Trader](https://github.com/RolandPheasant/Dynamic.Trader) which is an example of how Dynamic Data can handle fast moving high throughput trading data with. It illustrates some of the core operators of dynamic data and how a single data source can be shared and shaped in various ways. It also includes an example of how it can be integrated with ReactiveUI. 47 | 48 | ![Dynamic Trader](https://github.com/RolandPheasant/TradingDemo/blob/master/Images/LiveTrades.gif) 49 | 50 | [TailBlazer](https://github.com/RolandPheasant/TailBlazer) is a popular file tail utility which is an example of Rx and Dynamic Data and I consider to be a celebration of reactive programming. It is an advanced example of how to achieve high performance and how to lean on Rx and Dynamic Data to produce a slick and response user interface. 51 | 52 | ![Tail Blazer](https://github.com/RolandPheasant/TailBlazer/blob/master/Images/Release%20v0.9/Search%20and%20highlight.gif) 53 | 54 | [DynamicData Samplz](https://github.com/RolandPheasant/DynamicData.Samplz) where I started to do some visual examples but abandoned the project because I decided that the snippets project would be a better means of providing quick to produce examples. However there are still several good examples so well worth taking a look. 55 | 56 | ![Samplz](https://github.com/RolandPheasant/DynamicData.Samplz/blob/master/Images/Screenshot.gif) 57 | 58 | 59 | --------------------------------------------------------------------------------