├── .gitattributes ├── .github └── workflows │ ├── dotnet-core.yml │ └── onVersionTag.yml ├── .gitignore ├── BFF.DataVirtualizingCollection.Complete.sln ├── BFF.DataVirtualizingCollection.sln ├── BFF.DataVirtualizingCollection ├── BFF.DataVirtualizingCollection.csproj ├── DataVirtualizingCollection │ ├── AsyncDataVirtualizingCollection.cs │ ├── DataVirtualizingCollectionBase.cs │ ├── DataVirtualizingCollectionBuilder.cs │ ├── IDataVirtualizingCollection.cs │ └── SyncDataVirtualizingCollection.cs ├── DataVirtualizingCollectionBuilderBase.cs ├── IBuilderInterfaces.cs ├── IVirtualizationBase.cs ├── InternalsVisibleTo.cs ├── PageRemoval │ ├── HoardingPageNonRemoval.cs │ ├── LeastRecentlyUsedPageRemoval.cs │ └── PageReplacementStrategyException.cs ├── PageStorage │ ├── AsyncPageBase.cs │ ├── IAsyncPageFetchScheduler.cs │ ├── IPage.cs │ ├── ImmediateAsyncPageFetchScheduler.cs │ ├── LifoAsyncPageFetchScheduler.cs │ ├── PageStorage.cs │ ├── PreloadingPageStorage.cs │ └── SyncNonPreloadingPageBase.cs ├── Public.puml ├── SlidingWindow │ ├── ISlidingWindow.cs │ ├── SlidingWindow.cs │ └── SlidingWindowBuilder.cs ├── Utilities │ ├── DateTimeTimestampProvider.cs │ ├── ErrorMessage.cs │ └── ITimestampProvider.cs └── VirtualizationBase.cs ├── Directory.Build.props ├── Documentation ├── API.md └── Diagrams │ ├── Context.png │ ├── Context.uxf │ ├── DataVirtualizingCollection.png │ ├── DataVirtualizingCollection.svg │ ├── PublicInterfaceHierarchy.png │ ├── PublicInterfaceHierarchy.svg │ ├── PublicInterfaceHierarchy.uxf │ ├── SlidingWindow.png │ └── SlidingWindow.svg ├── Icon ├── Yeah69Logo.ico └── Yeah69Logo_256.png ├── LICENSE.md ├── README.md ├── Sample.GettingStarted ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs └── Sample.GettingStarted.csproj ├── Sample.Model ├── AutofacModule.cs ├── BackendAccesses │ ├── AllNumbersFakeBackendAccess.cs │ ├── HighWorkloadFakeBackendAccess.cs │ ├── IBackendAccess.cs │ ├── MillionNumbersBackendAccess.cs │ └── ProfilesFakeBackendAccess.cs ├── Models │ ├── HighWorkloadObject.cs │ ├── LowWorkloadObject.cs │ ├── Profile.cs │ └── SomeWorkloadObject.cs ├── PersistenceLink │ └── IFetchMillionNumbersFromBackend.cs └── Sample.Model.csproj ├── Sample.Persistence.Proxy ├── AutofacModule.cs └── Sample.Persistence.Proxy.csproj ├── Sample.Persistence ├── AutofacModule.cs ├── Databases │ └── BFF.DataVirtualizingCollection.MillionNumbers.sqlite ├── PersistenceLink │ └── FetchMillionNumbersFromBackend.cs └── Sample.Persistence.csproj ├── Sample.View ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── AutofacModule.cs ├── ProfilePics │ ├── 00_Wide.png │ ├── 01_Paria.png │ ├── 02_Morexandra.png │ ├── 03_Smennifer.png │ ├── 04_Anyetlana.png │ ├── 05_Korko.png │ ├── 06_Kowel.png │ ├── 07_Sinia.png │ ├── 08_Wonathan.png │ ├── 09_Matban.png │ ├── 10_Surgiana.png │ └── 11_Jogory.png ├── Resources │ ├── BackendCollectionDataTemplates.xaml │ └── ItemDataTemplates.xaml ├── Sample.View.csproj ├── Utilities │ ├── Converters.cs │ ├── DataTemplateSelectors.cs │ └── ProfileViewStatic.cs ├── ViewModelInterfaceImplementations │ └── GetScheduler.cs └── Views │ ├── DataVirtualizingCollectionView.xaml │ ├── DataVirtualizingCollectionView.xaml.cs │ ├── Decisions │ ├── FetcherKindView.xaml │ ├── FetcherKindView.xaml.cs │ ├── IndexAccessBehaviorViewModel.xaml │ ├── IndexAccessBehaviorViewModel.xaml.cs │ ├── PageLoadingBehaviorView.xaml │ ├── PageLoadingBehaviorView.xaml.cs │ ├── PageRemovalBehaviorView.xaml │ └── PageRemovalBehaviorView.xaml.cs │ ├── Functions │ ├── GeneralFunctionsView.xaml │ ├── GeneralFunctionsView.xaml.cs │ ├── SpecificFunctionsView.xaml │ └── SpecificFunctionsView.xaml.cs │ ├── MainWindowView.xaml │ ├── MainWindowView.xaml.cs │ └── Options │ ├── GeneralOptionsView.xaml │ ├── GeneralOptionsView.xaml.cs │ ├── SlidingWindowOptionsView.xaml │ └── SlidingWindowOptionsView.xaml.cs ├── Sample.ViewModel ├── Adapters │ ├── AllNumbersCollectionAdapter.cs │ ├── HighWorkloadCollectionAdapter.cs │ ├── IBackendAccessAdapter.cs │ ├── MillionNumbersCollectionAdapter.cs │ └── ProfileCollectionAdapter.cs ├── AutofacModule.cs ├── Interfaces │ └── IGetSchedulers.cs ├── Sample.ViewModel.csproj ├── Utility │ ├── RxRelayCommand.cs │ └── TransformingBackendAccess.cs └── ViewModels │ ├── DataVirtualizingCollectionViewModel.cs │ ├── Decisions │ ├── FetcherKindViewModel.cs │ ├── IndexAccessBehaviorViewModel.cs │ ├── PageLoadingBehaviorViewModel.cs │ └── PageRemovalBehaviorViewModel.cs │ ├── Functions │ ├── GeneralFunctionsViewModel.cs │ └── SpecificFunctionsViewModel.cs │ ├── MainWindowViewModel.cs │ ├── ObservableObject.cs │ ├── Options │ ├── GeneralOptionsViewModel.cs │ └── SlidingWindowOptionsViewModel.cs │ ├── ProfileViewModel.cs │ └── SomeWorkloadObjectViewModel.cs ├── Tests.Integration ├── DataVirtualizingCollectionDataAccess │ ├── AsyncDataAccessTests.cs │ └── SyncDataAccessTests.cs ├── DataVirtualizingCollectionFactory.cs ├── FactoryEnums.cs ├── PageRemoval │ ├── AsyncHoardingTests.cs │ ├── AsyncLeastRecentlyUsedTests.cs │ ├── SyncHoardingTests.cs │ └── SyncLeastRecentlyUsedTests.cs ├── SlidingWindowDataAccess │ ├── AsyncDataAccessTests.cs │ └── SyncDataAccessTests.cs ├── SlidingWindowFactory.cs ├── SlidingWindowSpecific │ ├── AsyncDataAccessTests.cs │ └── SyncDataAccessTests.cs └── Tests.Integration.csproj └── Tests.Unit ├── DataVirtualizingCollection └── AsyncDataVirtualizingCollectionTests.cs ├── PageRemoval ├── HoardingPageNonRemovalTests.cs └── LeastRecentlyUsedPageRemovalTests.cs ├── PageStorage ├── AsyncPageBaseTests.cs ├── PageStorageTests.cs ├── PageTestsBase.cs ├── PreloadingPageStorageTests.cs └── SyncNonPreloadingPageBaseTests.cs └── Tests.Unit.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: Build & Unit test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | windows: 11 | 12 | strategy: 13 | matrix: 14 | configuration: [ Release ] 15 | runs-on: windows-latest 16 | 17 | env: 18 | Solution_Name: BFF.DataVirtualizingCollection.sln # Replace with your solution name, i.e. MyWpfApp.sln. 19 | Test_Project_Path: Tests.Unit\Tests.Unit.csproj # Replace with the path to your test project, i.e. MyWpfApp.Tests\MyWpfApp.Tests.csproj. 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | - name: Setup .NET Core 26 | uses: actions/setup-dotnet@v1 27 | with: 28 | dotnet-version: 5.0.102 29 | - run: set DOTNET_CLI_TELEMETRY_OPTOUT=1 30 | - name: Install dependencies 31 | run: dotnet restore .\BFF.DataVirtualizingCollection.sln 32 | - name: Build 33 | run: dotnet build .\BFF.DataVirtualizingCollection.sln --configuration Release --no-restore 34 | - name: Test 35 | run: dotnet test .\Tests.Unit\Tests.Unit.csproj --no-restore --verbosity normal 36 | -------------------------------------------------------------------------------- /.github/workflows/onVersionTag.yml: -------------------------------------------------------------------------------- 1 | name: OnVersionTag 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | jobs: 8 | windows: 9 | 10 | strategy: 11 | matrix: 12 | configuration: [ Release ] 13 | runs-on: windows-latest 14 | 15 | env: 16 | Solution_Name: BFF.DataVirtualizingCollection.sln # Replace with your solution name, i.e. MyWpfApp.sln. 17 | Test_Project_Path: Tests.Unit\Tests.Unit.csproj # Replace with the path to your test project, i.e. MyWpfApp.Tests\MyWpfApp.Tests.csproj. 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Setup .NET Core 22 | uses: actions/setup-dotnet@v1 23 | with: 24 | dotnet-version: 5.0.102 25 | - run: set DOTNET_CLI_TELEMETRY_OPTOUT=1 26 | - name: Install dependencies 27 | run: dotnet restore .\BFF.DataVirtualizingCollection.sln 28 | - name: Build 29 | run: dotnet build .\BFF.DataVirtualizingCollection.sln --configuration Release --no-restore 30 | - name: Test 31 | run: dotnet test .\Tests.Unit\Tests.Unit.csproj --no-restore --verbosity normal 32 | - name: Publish to NuGet 33 | uses: brandedoutcast/publish-nuget@v2 34 | with: 35 | PROJECT_FILE_PATH: BFF.DataVirtualizingCollection/BFF.DataVirtualizingCollection.csproj 36 | VERSION_FILE_PATH: Directory.Build.props 37 | TAG_COMMIT: false 38 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | .vscode/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | 154 | # Windows Azure Build Output 155 | csx/ 156 | *.build.csdef 157 | 158 | # Windows Store app package directory 159 | AppPackages/ 160 | 161 | # Visual Studio cache files 162 | # files ending in .cache can be ignored 163 | *.[Cc]ache 164 | # but keep track of directories ending in .cache 165 | !*.[Cc]ache/ 166 | 167 | # Others 168 | ClientBin/ 169 | [Ss]tyle[Cc]op.* 170 | ~$* 171 | *~ 172 | *.dbmdl 173 | *.dbproj.schemaview 174 | *.pfx 175 | *.publishsettings 176 | node_modules/ 177 | orleans.codegen.cs 178 | 179 | # RIA/Silverlight projects 180 | Generated_Code/ 181 | 182 | # Backup & report files from converting an old project file 183 | # to a newer Visual Studio version. Backup files are not needed, 184 | # because we have git ;-) 185 | _UpgradeReport_Files/ 186 | Backup*/ 187 | UpgradeLog*.XML 188 | UpgradeLog*.htm 189 | 190 | # SQL Server files 191 | *.mdf 192 | *.ldf 193 | 194 | # Business Intelligence projects 195 | *.rdl.data 196 | *.bim.layout 197 | *.bim_*.settings 198 | 199 | # Microsoft Fakes 200 | FakesAssemblies/ 201 | 202 | # Node.js Tools for Visual Studio 203 | .ntvs_analysis.dat 204 | 205 | # Visual Studio 6 build log 206 | *.plg 207 | 208 | # Visual Studio 6 workspace options file 209 | *.opt 210 | 211 | # Visual Studio LightSwitch build output 212 | **/*.HTMLClient/GeneratedArtifacts 213 | **/*.DesktopClient/GeneratedArtifacts 214 | **/*.DesktopClient/ModelManifest.xml 215 | **/*.Server/GeneratedArtifacts 216 | **/*.Server/ModelManifest.xml 217 | _Pvt_Extensions 218 | 219 | # JetBrains IDEA 220 | .idea/ -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection.Complete.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28917.182 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BFF.DataVirtualizingCollection", "BFF.DataVirtualizingCollection\BFF.DataVirtualizingCollection.csproj", "{D4D769DA-F7A2-43F0-ADA7-5895D36D585F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Icon", "Icon", "{2536F943-EEC8-4CF7-B0B2-E3569E164152}" 9 | ProjectSection(SolutionItems) = preProject 10 | Icon\Yeah69Logo.ico = Icon\Yeah69Logo.ico 11 | Icon\Yeah69Logo_256.png = Icon\Yeah69Logo_256.png 12 | EndProjectSection 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.ViewModel", "Sample.ViewModel\Sample.ViewModel.csproj", "{A12A5816-3F2E-4DC6-A73A-231CA8EB56E9}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Model", "Sample.Model\Sample.Model.csproj", "{546A9B8C-0BA6-4FDC-802F-878F460D1211}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Persistence", "Sample.Persistence\Sample.Persistence.csproj", "{36C00EF5-5A92-4545-B6F5-6320F8C52C63}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Persistence.Proxy", "Sample.Persistence.Proxy\Sample.Persistence.Proxy.csproj", "{A3852A8E-01B4-4B70-8D3A-B168E042008B}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.View", "Sample.View\Sample.View.csproj", "{29AB6C8F-A71B-4D87-9C64-C0BE3B5CE229}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{DB002923-3355-4FBC-92E1-CC0FDFA3FCAD}" 25 | ProjectSection(SolutionItems) = preProject 26 | Directory.Build.props = Directory.Build.props 27 | EndProjectSection 28 | EndProject 29 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Unit", "Tests.Unit\Tests.Unit.csproj", "{D51F5D54-EDD2-4334-8CAE-2988C26B9E3F}" 30 | EndProject 31 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Integration", "Tests.Integration\Tests.Integration.csproj", "{2B1264E8-045B-454B-8485-4A6F415955FE}" 32 | EndProject 33 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.GettingStarted", "Sample.GettingStarted\Sample.GettingStarted.csproj", "{FEC9D854-809C-46AE-BA29-4A0C60CCBB3A}" 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {D4D769DA-F7A2-43F0-ADA7-5895D36D585F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {D4D769DA-F7A2-43F0-ADA7-5895D36D585F}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {D4D769DA-F7A2-43F0-ADA7-5895D36D585F}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {D4D769DA-F7A2-43F0-ADA7-5895D36D585F}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {A12A5816-3F2E-4DC6-A73A-231CA8EB56E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {A12A5816-3F2E-4DC6-A73A-231CA8EB56E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {A12A5816-3F2E-4DC6-A73A-231CA8EB56E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {A12A5816-3F2E-4DC6-A73A-231CA8EB56E9}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {546A9B8C-0BA6-4FDC-802F-878F460D1211}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {546A9B8C-0BA6-4FDC-802F-878F460D1211}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {546A9B8C-0BA6-4FDC-802F-878F460D1211}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {546A9B8C-0BA6-4FDC-802F-878F460D1211}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {36C00EF5-5A92-4545-B6F5-6320F8C52C63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {36C00EF5-5A92-4545-B6F5-6320F8C52C63}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {36C00EF5-5A92-4545-B6F5-6320F8C52C63}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {36C00EF5-5A92-4545-B6F5-6320F8C52C63}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {A3852A8E-01B4-4B70-8D3A-B168E042008B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {A3852A8E-01B4-4B70-8D3A-B168E042008B}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {A3852A8E-01B4-4B70-8D3A-B168E042008B}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {A3852A8E-01B4-4B70-8D3A-B168E042008B}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {29AB6C8F-A71B-4D87-9C64-C0BE3B5CE229}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {29AB6C8F-A71B-4D87-9C64-C0BE3B5CE229}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {29AB6C8F-A71B-4D87-9C64-C0BE3B5CE229}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {29AB6C8F-A71B-4D87-9C64-C0BE3B5CE229}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {D51F5D54-EDD2-4334-8CAE-2988C26B9E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {D51F5D54-EDD2-4334-8CAE-2988C26B9E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {D51F5D54-EDD2-4334-8CAE-2988C26B9E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {D51F5D54-EDD2-4334-8CAE-2988C26B9E3F}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {2B1264E8-045B-454B-8485-4A6F415955FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {2B1264E8-045B-454B-8485-4A6F415955FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {2B1264E8-045B-454B-8485-4A6F415955FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {2B1264E8-045B-454B-8485-4A6F415955FE}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {FEC9D854-809C-46AE-BA29-4A0C60CCBB3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {FEC9D854-809C-46AE-BA29-4A0C60CCBB3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {FEC9D854-809C-46AE-BA29-4A0C60CCBB3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {FEC9D854-809C-46AE-BA29-4A0C60CCBB3A}.Release|Any CPU.Build.0 = Release|Any CPU 77 | EndGlobalSection 78 | GlobalSection(SolutionProperties) = preSolution 79 | HideSolutionNode = FALSE 80 | EndGlobalSection 81 | GlobalSection(ExtensibilityGlobals) = postSolution 82 | SolutionGuid = {06A68F93-74CC-4C15-BD02-62B43AB8A585} 83 | EndGlobalSection 84 | EndGlobal 85 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28917.182 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BFF.DataVirtualizingCollection", "BFF.DataVirtualizingCollection\BFF.DataVirtualizingCollection.csproj", "{D4D769DA-F7A2-43F0-ADA7-5895D36D585F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Icon", "Icon", "{2536F943-EEC8-4CF7-B0B2-E3569E164152}" 9 | ProjectSection(SolutionItems) = preProject 10 | Icon\Yeah69Logo.ico = Icon\Yeah69Logo.ico 11 | Icon\Yeah69Logo_256.png = Icon\Yeah69Logo_256.png 12 | EndProjectSection 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{DB002923-3355-4FBC-92E1-CC0FDFA3FCAD}" 15 | ProjectSection(SolutionItems) = preProject 16 | Directory.Build.props = Directory.Build.props 17 | EndProjectSection 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Integration", "Tests.Integration\Tests.Integration.csproj", "{3F7BB7A3-7950-45B9-9968-3BBF6F186B30}" 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Unit", "Tests.Unit\Tests.Unit.csproj", "{E0EBFFF9-3F4F-4E81-AC3D-69937E0393CA}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {D4D769DA-F7A2-43F0-ADA7-5895D36D585F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {D4D769DA-F7A2-43F0-ADA7-5895D36D585F}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {D4D769DA-F7A2-43F0-ADA7-5895D36D585F}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {D4D769DA-F7A2-43F0-ADA7-5895D36D585F}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {3F7BB7A3-7950-45B9-9968-3BBF6F186B30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {3F7BB7A3-7950-45B9-9968-3BBF6F186B30}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {3F7BB7A3-7950-45B9-9968-3BBF6F186B30}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {3F7BB7A3-7950-45B9-9968-3BBF6F186B30}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {E0EBFFF9-3F4F-4E81-AC3D-69937E0393CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {E0EBFFF9-3F4F-4E81-AC3D-69937E0393CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {E0EBFFF9-3F4F-4E81-AC3D-69937E0393CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {E0EBFFF9-3F4F-4E81-AC3D-69937E0393CA}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {06A68F93-74CC-4C15-BD02-62B43AB8A585} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/BFF.DataVirtualizingCollection.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | BFF.DataVirtualizingCollection 6 | This is an approach to data virtualizing collections intended to be used in WPF projects. 7 | false 8 | - breaking changes compared to version 2.* and before 9 | - no blocking calls anymore 10 | - separate background scheduler for count fetching, page fetching and preloading configurable 11 | 12 | data-virtualization wpf csharp 13 | true 14 | https://github.com/Yeah69/BFF.DataVirtualizingCollection 15 | https://github.com/Yeah69/BFF.DataVirtualizingCollection.git 16 | git 17 | Yeah69Logo_256.png 18 | 19 | LICENSE.md 20 | ../Icon/Yeah69_256.png 21 | BFF.DataVirtualizingCollection 22 | true 23 | $(BaseIntermediateOutputPath)\GeneratedFiles 24 | 25 | 26 | 27 | bin\Release\netstandard1.4\BFF.DataVirtualizingCollection.xml 28 | 29 | 30 | 31 | bin\Debug\BFF.DataVirtualizingCollection.xml 32 | 33 | 34 | 35 | ..\Documentation\API.md 36 | 37 | 38 | 39 | 40 | 41 | all 42 | runtime; build; native; contentfiles; analyzers; buildtransitive 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | True 53 | 54 | 55 | 56 | 57 | 58 | 59 | true 60 | 61 | 62 | 63 | true 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/DataVirtualizingCollection/AsyncDataVirtualizingCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive; 3 | using System.Reactive.Concurrency; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | using System.Reactive.Subjects; 7 | using System.Reactive.Threading.Tasks; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using BFF.DataVirtualizingCollection.PageStorage; 11 | using MrMeeseeks.Reactive.Extensions; 12 | 13 | namespace BFF.DataVirtualizingCollection.DataVirtualizingCollection 14 | { 15 | internal sealed class AsyncDataVirtualizingCollection : DataVirtualizingCollectionBase 16 | { 17 | private readonly Func> _countFetcher; 18 | private readonly IScheduler _notificationScheduler; 19 | private readonly IScheduler _countBackgroundScheduler; 20 | private readonly IPageStorage _pageStorage; 21 | private readonly Subject _resetSubject = new (); 22 | private readonly SerialDisposable _pendingCountRequestCancellation = new (); 23 | 24 | private int _count; 25 | 26 | internal AsyncDataVirtualizingCollection( 27 | Func> pageStoreFactory, 28 | Func> countFetcher, 29 | IObservable<(int Offset, int PageSize, T[] PreviousPage, T[] Page)> observePageFetches, 30 | IDisposable disposeOnDisposal, 31 | IScheduler notificationScheduler, 32 | IScheduler countBackgroundScheduler) 33 | : base(observePageFetches, disposeOnDisposal, notificationScheduler) 34 | { 35 | _countFetcher = countFetcher; 36 | _notificationScheduler = notificationScheduler; 37 | _countBackgroundScheduler = countBackgroundScheduler; 38 | _count = 0; 39 | 40 | _resetSubject.CompositeDisposalWith(CompositeDisposable); 41 | 42 | _pageStorage = pageStoreFactory(0); 43 | 44 | InitializationCompleted = _resetSubject.FirstAsync().ToTask(); 45 | 46 | _resetSubject 47 | .SelectMany(async ct => 48 | { 49 | await ResetInner(ct).ConfigureAwait(false); 50 | return Unit.Default; 51 | }) 52 | .Subscribe(_ => {}) 53 | .CompositeDisposalWith(CompositeDisposable); 54 | 55 | Reset(); 56 | } 57 | 58 | public override int Count => _count; 59 | 60 | protected override T GetItemInner(int index) => _pageStorage[index]; 61 | 62 | private async Task ResetInner(CancellationToken ct) 63 | { 64 | try 65 | { 66 | await Observable.FromAsync(_countFetcher, _countBackgroundScheduler) 67 | .SelectMany(async count => 68 | { 69 | _count = count; 70 | await _pageStorage.Reset(_count).ConfigureAwait(false); 71 | return Unit.Default; 72 | }) 73 | .ObserveOn(_notificationScheduler) 74 | .Do(_ => 75 | { 76 | OnPropertyChanged(nameof(Count)); 77 | OnCollectionChangedReset(); 78 | OnIndexerChanged(); 79 | }) 80 | .ToTask(ct) 81 | .ConfigureAwait(false); 82 | } 83 | catch (OperationCanceledException) 84 | { 85 | // ignore cancellation from now on 86 | } 87 | } 88 | 89 | public override void Reset() 90 | { 91 | var cancellationDisposable = new CancellationDisposable(); 92 | _pendingCountRequestCancellation.Disposable = cancellationDisposable; 93 | _resetSubject.OnNext(cancellationDisposable.Token); 94 | } 95 | 96 | public override Task InitializationCompleted { get; } 97 | 98 | public override async ValueTask DisposeAsync() 99 | { 100 | _pendingCountRequestCancellation.Dispose(); 101 | await base.DisposeAsync().ConfigureAwait(false); 102 | await _pageStorage.DisposeAsync().ConfigureAwait(false); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/DataVirtualizingCollection/DataVirtualizingCollectionBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Concurrency; 3 | using System.Reactive.Linq; 4 | using MrMeeseeks.Reactive.Extensions; 5 | 6 | namespace BFF.DataVirtualizingCollection.DataVirtualizingCollection 7 | { 8 | internal abstract class DataVirtualizingCollectionBase : VirtualizationBase, IDataVirtualizingCollection 9 | { 10 | private int _selectedIndex; 11 | 12 | protected DataVirtualizingCollectionBase( 13 | IObservable<(int Offset, int PageSize, T[] PreviousPage, T[] Page)> observePageFetches, 14 | IDisposable disposeOnDisposal, 15 | IScheduler notificationScheduler) 16 | { 17 | disposeOnDisposal.CompositeDisposalWith(CompositeDisposable); 18 | 19 | observePageFetches 20 | .ObserveOn(notificationScheduler) 21 | .Subscribe(t => 22 | { 23 | var (offset, pageSize, previousPage, page) = t; 24 | for (var i = 0; i < pageSize; i++) 25 | { 26 | OnCollectionChangedReplace(page[i], previousPage[i], i + offset); 27 | } 28 | OnIndexerChanged(); 29 | }) 30 | .CompositeDisposalWith(CompositeDisposable); 31 | } 32 | 33 | protected override T IndexerInnerGet(int index) => 34 | index >= Count || index < 0 35 | ? throw new IndexOutOfRangeException("Index was out of range. Must be non-negative and less than the size of the collection.") 36 | : GetItemInner(index); 37 | 38 | protected abstract T GetItemInner(int index); 39 | 40 | private int _preResetSelectedIndex = -1; 41 | 42 | protected override void OnCollectionChangedReset() 43 | { 44 | _preResetSelectedIndex = _selectedIndex; 45 | SelectedIndex = -1; // deselection in order to workaround issue of Selectors 46 | base.OnCollectionChangedReset(); 47 | SelectedIndex = _preResetSelectedIndex; 48 | } 49 | 50 | protected override T GetItemForEnumerator(int i) => GetItemInner(i); 51 | 52 | public override int SelectedIndex 53 | { 54 | get => _selectedIndex; 55 | set 56 | { 57 | if (_selectedIndex == value) return; 58 | _selectedIndex = value; 59 | OnPropertyChanged(); 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/DataVirtualizingCollection/DataVirtualizingCollectionBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.ComponentModel; 4 | using System.Reactive.Concurrency; 5 | using System.Reactive.Linq; 6 | using System.Reactive.Subjects; 7 | using System.Threading.Tasks; 8 | 9 | namespace BFF.DataVirtualizingCollection.DataVirtualizingCollection 10 | { 11 | /// 12 | /// Initial entry point for creating a data virtualizing collection. 13 | /// 14 | public static class DataVirtualizingCollectionBuilder 15 | { 16 | /// 17 | /// Use to configure general virtualization settings. 18 | /// Further settings are applied via method chaining. 19 | /// Page size is set to the default value 100. 20 | /// The background scheduler is per default the . 21 | /// 22 | /// A scheduler for sending the notifications (, ). 23 | public static IPageLoadingBehaviorCollectionBuilder> Build( 24 | IScheduler notificationScheduler) => 25 | Build(DataVirtualizingCollectionBuilderBase.DefaultPageSize, notificationScheduler); 26 | 27 | /// 28 | /// Use to configure general virtualization settings. 29 | /// Further settings are applied via method chaining. 30 | /// The background scheduler is per default the . 31 | /// 32 | /// Maximum size of a single page. 33 | /// A scheduler for sending the notifications (, ). 34 | public static IPageLoadingBehaviorCollectionBuilder> Build( 35 | int pageSize, 36 | IScheduler notificationScheduler) => 37 | new DataVirtualizingCollectionBuilder(pageSize, notificationScheduler); 38 | 39 | /// 40 | /// Use to configure general virtualization settings. 41 | /// Further settings are applied via method chaining. 42 | /// 43 | /// Maximum size of a single page. 44 | /// A scheduler for sending the notifications (, ). 45 | /// Per default this scheduler is used for all background operations (page and count fetches, preloading). In further settings you'll have the option to override this scheduler with another for specific background operations. 46 | public static IPageLoadingBehaviorCollectionBuilder> Build( 47 | int pageSize, 48 | IScheduler notificationScheduler, 49 | IScheduler backgroundScheduler) => 50 | new DataVirtualizingCollectionBuilder(pageSize, notificationScheduler, backgroundScheduler); 51 | } 52 | 53 | internal sealed class DataVirtualizingCollectionBuilder 54 | : DataVirtualizingCollectionBuilderBase> 55 | { 56 | 57 | internal DataVirtualizingCollectionBuilder(int pageSize, IScheduler notificationScheduler) 58 | : base(pageSize, notificationScheduler) 59 | { 60 | } 61 | 62 | internal DataVirtualizingCollectionBuilder(int pageSize, IScheduler notificationScheduler, IScheduler backgroundScheduler) 63 | : base(pageSize, notificationScheduler, backgroundScheduler) 64 | { 65 | } 66 | 67 | protected override IDataVirtualizingCollection GenerateTaskBasedAsynchronousCollection( 68 | Subject<(int Offset, int PageSize, TItem[] PreviousPage, TItem[] Page)> pageFetchEvents) 69 | { 70 | var taskBasedCountFetcher = TaskBasedCountFetcher ?? 71 | throw new NullReferenceException(UninitializedElementsExceptionMessage); 72 | return new AsyncDataVirtualizingCollection( 73 | GenerateTaskBasedAsynchronousPageStorage(pageFetchEvents), 74 | taskBasedCountFetcher, 75 | pageFetchEvents.AsObservable(), 76 | pageFetchEvents, 77 | NotificationScheduler, 78 | CountBackgroundScheduler); 79 | } 80 | 81 | protected override IDataVirtualizingCollection GenerateAsyncEnumerableBasedAsynchronousCollection(Subject<(int Offset, int PageSize, TItem[] PreviousPage, TItem[] Page)> pageFetchEvents) 82 | { 83 | var taskBasedCountFetcher = TaskBasedCountFetcher ?? 84 | throw new NullReferenceException(UninitializedElementsExceptionMessage); 85 | return new AsyncDataVirtualizingCollection( 86 | GenerateAsyncEnumerableBasedAsynchronousPageStorage(pageFetchEvents), 87 | taskBasedCountFetcher, 88 | pageFetchEvents.AsObservable(), 89 | pageFetchEvents, 90 | NotificationScheduler, 91 | CountBackgroundScheduler); 92 | } 93 | 94 | protected override IDataVirtualizingCollection GenerateNonTaskBasedAsynchronousCollection( 95 | Subject<(int Offset, int PageSize, TItem[] PreviousPage, TItem[] Page)> pageFetchEvents) 96 | { 97 | var countFetcher = CountFetcher ?? throw new NullReferenceException(UninitializedElementsExceptionMessage); 98 | return new AsyncDataVirtualizingCollection( 99 | GenerateNonTaskBasedAsynchronousPageStorage(pageFetchEvents), 100 | ct => Task.FromResult(countFetcher(ct)), 101 | pageFetchEvents.AsObservable(), 102 | pageFetchEvents, 103 | NotificationScheduler, 104 | CountBackgroundScheduler); 105 | } 106 | 107 | protected override IDataVirtualizingCollection GenerateNonTaskBasedSynchronousCollection( 108 | Subject<(int Offset, int PageSize, TItem[] PreviousPage, TItem[] Page)> pageFetchEvents) 109 | { 110 | var countFetcher = CountFetcher ?? throw new NullReferenceException(UninitializedElementsExceptionMessage); 111 | return new SyncDataVirtualizingCollection( 112 | GenerateNonTaskBasedSynchronousPageStorage(pageFetchEvents), 113 | countFetcher, 114 | pageFetchEvents.AsObservable(), 115 | pageFetchEvents, 116 | NotificationScheduler); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/DataVirtualizingCollection/IDataVirtualizingCollection.cs: -------------------------------------------------------------------------------- 1 | namespace BFF.DataVirtualizingCollection.DataVirtualizingCollection 2 | { 3 | // ReSharper disable once PossibleInterfaceMemberAmbiguity 4 | // Ambiguous Members should be implemented explicitly 5 | /// 6 | /// Marks a nongeneric data virtualizing collection. 7 | /// The data virtualizing collection represents the whole backend as a list. However, the items are not loaded all at once but page by page on demand. 8 | /// 9 | public interface IDataVirtualizingCollection : IVirtualizationBase 10 | { 11 | } 12 | 13 | // ReSharper disable once PossibleInterfaceMemberAmbiguity 14 | // Ambiguous Members should be implemented explicitly 15 | /// 16 | /// Marks a generic data virtualizing collection. 17 | /// The data virtualizing collection represents the whole backend as a list. However, the items are not loaded all at once but page by page on demand. 18 | /// 19 | /// Item type. 20 | public interface IDataVirtualizingCollection : 21 | IVirtualizationBase, 22 | IDataVirtualizingCollection 23 | { 24 | } 25 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/DataVirtualizingCollection/SyncDataVirtualizingCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive; 3 | using System.Reactive.Concurrency; 4 | using System.Reactive.Subjects; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using BFF.DataVirtualizingCollection.PageStorage; 8 | using MrMeeseeks.Reactive.Extensions; 9 | 10 | namespace BFF.DataVirtualizingCollection.DataVirtualizingCollection 11 | { 12 | internal sealed class SyncDataVirtualizingCollection : DataVirtualizingCollectionBase 13 | { 14 | private readonly Func _countFetcher; 15 | private readonly IScheduler _notificationScheduler; 16 | private readonly IPageStorage _pageStorage; 17 | private readonly Subject _resetSubject = new(); 18 | 19 | private int _count; 20 | 21 | internal SyncDataVirtualizingCollection( 22 | Func> pageStoreFactory, 23 | Func countFetcher, 24 | IObservable<(int Offset, int PageSize, T[] PreviousPage, T[] Page)> observePageFetches, 25 | IDisposable disposeOnDisposal, 26 | IScheduler notificationScheduler) 27 | : base (observePageFetches, disposeOnDisposal, notificationScheduler) 28 | { 29 | _countFetcher = countFetcher; 30 | _notificationScheduler = notificationScheduler; 31 | _count = _countFetcher(CancellationToken.None); 32 | _pageStorage = pageStoreFactory(_count); 33 | 34 | _resetSubject.CompositeDisposalWith(CompositeDisposable); 35 | 36 | _resetSubject 37 | .Subscribe(_ => ResetInner()) 38 | .CompositeDisposalWith(CompositeDisposable); 39 | } 40 | 41 | public override int Count => _count; 42 | 43 | protected override T GetItemInner(int index) 44 | { 45 | return _pageStorage[index]; 46 | } 47 | 48 | private void ResetInner() 49 | { 50 | _count = _countFetcher(CancellationToken.None); 51 | _pageStorage.Reset(_count); 52 | _notificationScheduler.Schedule(Unit.Default, (_, __) => 53 | { 54 | OnPropertyChanged(nameof(Count)); 55 | OnCollectionChangedReset(); 56 | OnIndexerChanged(); 57 | }); 58 | } 59 | 60 | public override void Reset() => _resetSubject.OnNext(Unit.Default); 61 | 62 | public override Task InitializationCompleted { get; } = Task.CompletedTask; 63 | 64 | public override async ValueTask DisposeAsync() 65 | { 66 | await base.DisposeAsync().ConfigureAwait(false); 67 | await _pageStorage.DisposeAsync().ConfigureAwait(false); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/IVirtualizationBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.Specialized; 5 | using System.ComponentModel; 6 | using System.Threading.Tasks; 7 | 8 | namespace BFF.DataVirtualizingCollection 9 | { 10 | /// 11 | /// Root interface of all virtualizing collection types. Implements all necessary .Net interfaces (IList, INotifyCollectionChanged, INotifyPropertyChanged) in order to be usable by standard UI controls (like ItemsControl from WPF). 12 | /// Additionally, it has virtualization-specific members of its own. See documentation of the members for further details. 13 | /// 14 | /// 15 | /// Implements IList in order to mock a standard .Net list. Controls like the ItemsControl from WPF in combination with the VirtualizingStackPanel use the indexer in order to load items on demand. Also the Count-property is used to arrange the scroll bar accordingly. 16 | /// The virtualized collections automatically changes its items. For example, if an async index access or preloading is set up, it will be eventually necessary to replace placeholders with the actually loaded items. In order to notify this changes to the UI the INotifyCollectionChanged interface is implemented. 17 | /// Also the properties of the collection itself may change, especially the Count-property. Hence notifications for such changes are send with INotifyPropertyChanged. 18 | /// Finally, each virtualizing collection has to be disposed after use, because it uses Rx subscriptions which have to be cleaned up. Also the currently active pages including their items which implement IDisposable are disposed as well. 19 | /// 20 | public interface IVirtualizationBase : 21 | IList, 22 | INotifyCollectionChanged, 23 | INotifyPropertyChanged, 24 | IAsyncDisposable 25 | { 26 | /// 27 | /// Task is successfully completed when initialization is completed. 28 | /// 29 | /// 30 | /// Initialization depends on the initial calculation of the Count-property. Because of the asynchronicity of task-based fetchers the Count-property might not be calculated at the end of construction of the virtualized collection. 31 | /// 32 | Task InitializationCompleted { get; } 33 | 34 | /// 35 | /// Can be bound to SelectedIndexProperty on Selector controls in order to workaround issue with resets and selected items. 36 | /// 37 | /// 38 | /// In WPF the Selector control will search for the previously selected item after each reset by iterating over all items until found. This behavior is the opposite of virtualization. Hence, the virtualizing collection would set this property to -1 (deselection) and notify the change before notifying any reset. 39 | /// 40 | int SelectedIndex { get; set; } 41 | 42 | /// 43 | /// Disposes of all current pages and notifies that possibly everything changed. 44 | /// The Reset-function should be called any time when something in the virtualized backend has changed. 45 | /// 46 | /// 47 | /// Consequently, the Count-property is recalculated. The UI will request all currently rendered items anew, so this items get reloaded. 48 | /// 49 | void Reset(); 50 | } 51 | 52 | /// 53 | /// The generic version of . Analogously, it implements and . 54 | /// 55 | /// Item type. 56 | // ReSharper disable once PossibleInterfaceMemberAmbiguity *** the "new" members of this interface resolve the ambiguities 57 | public interface IVirtualizationBase : 58 | IVirtualizationBase, 59 | IList, 60 | IReadOnlyList 61 | { 62 | /// 63 | /// The Count-property is newed here in order to resolve ambiguities cause by implementing , and at the same time. 64 | /// 65 | new int Count { get; } 66 | 67 | /// 68 | /// The indexer is newed here in order to resolve ambiguities cause by implementing , and at the same time. 69 | /// 70 | new T this[int index] { get; } 71 | } 72 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | [assembly: InternalsVisibleTo("BFF.DataVirtualizingCollection.Tests.Unit")] 3 | [assembly: InternalsVisibleTo("BFF.DataVirtualizingCollection.IntegrationTests")] 4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageRemoval/HoardingPageNonRemoval.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive.Linq; 4 | 5 | namespace BFF.DataVirtualizingCollection.PageRemoval 6 | { 7 | internal static class HoardingPageNonRemoval 8 | { 9 | internal static Func, IObservable>> Create() => 10 | _ => Observable.Never>(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageRemoval/LeastRecentlyUsedPageRemoval.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Reactive.Linq; 6 | using BFF.DataVirtualizingCollection.Utilities; 7 | 8 | namespace BFF.DataVirtualizingCollection.PageRemoval 9 | { 10 | internal static class LeastRecentlyUsedPageRemoval 11 | { 12 | internal static Func, IObservable>> Create( 13 | int pageLimit, 14 | int removalCount, 15 | bool isPreloading, 16 | ITimestampProvider timestampProvider) 17 | { 18 | IDictionary lastUsage = new Dictionary(); 19 | return pageRequests => 20 | { 21 | if (isPreloading) 22 | { 23 | // Make sure at least three pages (because of the preloading the two pages need to be 24 | // considered for the neighbor pages of the last fetched page) stay in the page store 25 | pageLimit = pageLimit < 4 ? 4 : pageLimit; 26 | removalCount = removalCount < 1 27 | ? 1 28 | : removalCount > pageLimit - 3 29 | ? pageLimit - 3 30 | : removalCount; 31 | } 32 | else 33 | { 34 | // Make sure at least one page stays in the page store 35 | pageLimit = pageLimit < 2 ? 2 : pageLimit; 36 | removalCount = removalCount < 1 37 | ? 1 38 | : removalCount > pageLimit - 1 39 | ? pageLimit - 1 40 | : removalCount; 41 | } 42 | 43 | return pageRequests.Select(tuple => 44 | { 45 | lastUsage[tuple.PageKey] = timestampProvider.Now; 46 | 47 | // Don't request any page removals if page limit isn't reached 48 | if (lastUsage.Count <= pageLimit) return new ReadOnlyCollection(new List()); 49 | 50 | // Remove at least one page but as much as required to get below page limit 51 | // The "-1" is necessary because the currently requested page is included in the count of the last usages 52 | var actualRemovalCount = lastUsage.Count - 1 - pageLimit + removalCount; 53 | var removalRequests = new ReadOnlyCollection( 54 | lastUsage 55 | // Sort by least recent usage 56 | .OrderBy(kvp => kvp.Value) 57 | .Take(actualRemovalCount) 58 | .Select(kvp => kvp.Key) 59 | .ToList()); 60 | // Assuming the page will be removed, they need to be removed from the last-usage tracking here as well 61 | foreach (var removalRequest in removalRequests) 62 | { 63 | lastUsage.Remove(removalRequest); 64 | } 65 | 66 | return removalRequests; 67 | }) 68 | .Where(list => list.Any()); 69 | }; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageRemoval/PageReplacementStrategyException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BFF.DataVirtualizingCollection.PageRemoval 4 | { 5 | /// 6 | /// Thrown whenever an exception occurs during the process of page replacement. 7 | /// 8 | public class PageReplacementStrategyException : Exception 9 | { 10 | internal PageReplacementStrategyException(string message, Exception innerException) : base(message, innerException) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageStorage/IAsyncPageFetchScheduler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BFF.DataVirtualizingCollection.PageStorage 4 | { 5 | internal interface IAsyncPageFetchScheduler 6 | { 7 | Task Schedule(); 8 | } 9 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageStorage/IPage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace BFF.DataVirtualizingCollection.PageStorage 5 | { 6 | internal interface IPage : IAsyncDisposable 7 | { 8 | Task PageFetchCompletion { get; } 9 | } 10 | 11 | internal interface IPage : IPage 12 | { 13 | T this[int index] { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageStorage/ImmediateAsyncPageFetchScheduler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BFF.DataVirtualizingCollection.PageStorage 4 | { 5 | internal class ImmediateAsyncPageFetchScheduler : IAsyncPageFetchScheduler 6 | { 7 | public Task Schedule() => Task.CompletedTask; 8 | } 9 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageStorage/LifoAsyncPageFetchScheduler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive; 5 | using System.Reactive.Concurrency; 6 | using System.Reactive.Linq; 7 | using System.Reactive.Subjects; 8 | using System.Threading.Tasks; 9 | 10 | namespace BFF.DataVirtualizingCollection.PageStorage 11 | { 12 | internal class LifoAsyncPageFetchScheduler : IAsyncPageFetchScheduler 13 | { 14 | private readonly Stack> _stack = new(); 15 | private readonly Subject> _subject = new(); 16 | 17 | public LifoAsyncPageFetchScheduler( 18 | TimeSpan throttleDueTime, 19 | IScheduler pageRequestBackgroundScheduler) => 20 | _subject 21 | .Synchronize() 22 | .Do(tcs => _stack.Push(tcs)) 23 | .Throttle(throttleDueTime, pageRequestBackgroundScheduler) 24 | .Subscribe(_ => 25 | { 26 | while (_stack.Any()) 27 | { 28 | var tcs = _stack.Pop(); 29 | tcs.SetResult(Unit.Default); 30 | } 31 | }); 32 | 33 | public Task Schedule() 34 | { 35 | var taskCompletionSource = new TaskCompletionSource(); 36 | _subject.OnNext(taskCompletionSource); 37 | return taskCompletionSource.Task; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageStorage/PageStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reactive; 6 | using System.Reactive.Disposables; 7 | using System.Reactive.Linq; 8 | using System.Reactive.Subjects; 9 | using System.Threading.Tasks; 10 | using BFF.DataVirtualizingCollection.PageRemoval; 11 | using MrMeeseeks.Reactive.Extensions; 12 | 13 | namespace BFF.DataVirtualizingCollection.PageStorage 14 | { 15 | internal interface IPageStorage : IAsyncDisposable 16 | { 17 | T this[int index] { get; } 18 | 19 | Task Reset(int newCount); 20 | } 21 | 22 | internal class PageStorage : IPageStorage 23 | { 24 | private readonly int _pageSize; 25 | private readonly Func> _nonPreloadingPageFactory; 26 | private readonly CompositeDisposable _compositeDisposable = new(); 27 | 28 | protected bool IsDisposed; 29 | protected readonly object IsDisposedLock = new(); 30 | protected readonly ConcurrentDictionary> Pages = new(); 31 | protected readonly ISubject<(int PageKey, int PageIndex)> Requests; 32 | 33 | private int _count; 34 | 35 | internal PageStorage( 36 | int pageSize, 37 | int count, 38 | Func> nonPreloadingPageFactory, 39 | Func, IObservable>> pageReplacementStrategyFactory) 40 | { 41 | _pageSize = pageSize; 42 | _count = count; 43 | PageCount = CalculateCurrentPageCount(); 44 | _nonPreloadingPageFactory = nonPreloadingPageFactory; 45 | Requests = new Subject<(int PageKey, int PageIndex)>().CompositeDisposalWith(_compositeDisposable); 46 | pageReplacementStrategyFactory(Requests) 47 | .SelectMany(async pageKeysToRemove => 48 | { 49 | var disposables = new List(); 50 | foreach (var pageKey in pageKeysToRemove) 51 | { 52 | if (Pages.TryGetValue(pageKey, out var page)) 53 | disposables.Add(page.DisposeAsync().AsTask()); 54 | } 55 | 56 | await Task.WhenAll(disposables); 57 | return Unit.Default; 58 | }) 59 | .Subscribe(_ => { }, 60 | exception => throw new PageReplacementStrategyException( 61 | "LeastRecentlyUsed strategy: Something unexpected happened during page removal! See inner exception.", 62 | exception)) 63 | .CompositeDisposalWith(_compositeDisposable); 64 | } 65 | 66 | protected int PageCount { get; private set; } 67 | 68 | public T this[int index] 69 | { 70 | get 71 | { 72 | var pageKey = index / _pageSize; 73 | var pageIndex = index % _pageSize; 74 | 75 | lock (IsDisposedLock) 76 | { 77 | if (!IsDisposed) Requests.OnNext((pageKey, pageIndex)); 78 | } 79 | 80 | var ret = Pages 81 | .GetOrAdd( 82 | pageKey, 83 | FetchNonPreloadingPage) 84 | [pageIndex]; 85 | 86 | Preloading(pageKey); 87 | 88 | return ret; 89 | 90 | IPage FetchNonPreloadingPage(int key) => FetchPage(key, _nonPreloadingPageFactory); 91 | } 92 | } 93 | 94 | public Task Reset(int newCount) 95 | { 96 | _count = newCount; 97 | PageCount = CalculateCurrentPageCount(); 98 | 99 | return Task.WhenAll( 100 | Pages.Values.ToList().Select(p => p.DisposeAsync().AsTask())); 101 | 102 | } 103 | 104 | protected virtual void Preloading(int pageKey) 105 | { } 106 | 107 | protected IPage FetchPage(int key, Func> fetcher) 108 | { 109 | var offset = key * _pageSize; 110 | var actualPageSize = Math.Min(_pageSize, _count - offset); 111 | var disposable = Disposable.Create(() => Pages.TryRemove(key, out _)); 112 | return fetcher(key, offset, actualPageSize, disposable); 113 | } 114 | 115 | public async ValueTask DisposeAsync() 116 | { 117 | var pages = Pages.Values.ToList(); 118 | Pages.Clear(); 119 | 120 | await Task.WhenAll( 121 | pages.Select(p => p.DisposeAsync().AsTask())); 122 | 123 | lock (IsDisposedLock) 124 | { 125 | _compositeDisposable.Dispose(); 126 | IsDisposed = true; 127 | } 128 | } 129 | 130 | private int CalculateCurrentPageCount() => _count % _pageSize == 0 131 | ? _count / _pageSize 132 | : _count / _pageSize + 1; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageStorage/PreloadingPageStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BFF.DataVirtualizingCollection.PageStorage 5 | { 6 | internal class PreloadingPageStorage : PageStorage 7 | { 8 | private readonly Func> _preloadingPageFactory; 9 | 10 | internal PreloadingPageStorage( 11 | int pageSize, 12 | int count, 13 | Func> nonPreloadingPageFactory, 14 | Func> preloadingPageFactory, 15 | Func, IObservable>> pageReplacementStrategyFactory) 16 | : base ( 17 | pageSize, 18 | count, 19 | nonPreloadingPageFactory, 20 | pageReplacementStrategyFactory) 21 | { 22 | _preloadingPageFactory = preloadingPageFactory; 23 | } 24 | 25 | protected override void Preloading(int pageKey) 26 | { 27 | var nextPageKey = pageKey + 1; 28 | if (nextPageKey < PageCount) 29 | Pages.GetOrAdd( 30 | nextPageKey, 31 | FetchPreloadingPage); 32 | var previousPageKey = pageKey - 1; 33 | if (previousPageKey >= 0) 34 | Pages.GetOrAdd( 35 | previousPageKey, 36 | FetchPreloadingPage); 37 | 38 | IPage FetchPreloadingPage(int key) 39 | { 40 | lock (IsDisposedLock) 41 | { 42 | if (!IsDisposed) Requests.OnNext((key, -1)); 43 | } 44 | 45 | return FetchPage(key, _preloadingPageFactory); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/PageStorage/SyncNonPreloadingPageBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace BFF.DataVirtualizingCollection.PageStorage 7 | { 8 | internal class SyncNonPreloadingNonTaskBasedPage : IPage 9 | { 10 | private readonly int _pageSize; 11 | private readonly IDisposable _onDisposalAfterFetchCompleted; 12 | 13 | internal SyncNonPreloadingNonTaskBasedPage( 14 | // parameter 15 | int offset, 16 | int pageSize, 17 | IDisposable onDisposalAfterFetchCompleted, 18 | 19 | // dependencies 20 | Func pageFetcher) 21 | { 22 | _pageSize = pageSize; 23 | _onDisposalAfterFetchCompleted = onDisposalAfterFetchCompleted; 24 | PageContent = pageFetcher(offset, pageSize, CancellationToken.None); 25 | } 26 | 27 | private T[] PageContent { get; } 28 | 29 | public T this[int index] => 30 | index >= _pageSize || index < 0 31 | ? throw new IndexOutOfRangeException( 32 | "Index was out of range. Must be non-negative and less than the size of the collection.") 33 | : PageContent[index]; 34 | 35 | public Task PageFetchCompletion => Task.CompletedTask; 36 | 37 | public async ValueTask DisposeAsync() 38 | { 39 | await PageFetchCompletion.ConfigureAwait(false); 40 | _onDisposalAfterFetchCompleted.Dispose(); 41 | foreach (var disposable in PageContent.OfType()) 42 | { 43 | disposable.Dispose(); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/SlidingWindow/ISlidingWindow.cs: -------------------------------------------------------------------------------- 1 | namespace BFF.DataVirtualizingCollection.SlidingWindow 2 | { 3 | // ReSharper disable once PossibleInterfaceMemberAmbiguity 4 | // Ambiguous Members should be implemented explicitly 5 | /// 6 | /// Defines a nongeneric window to the backend (accessed by the page- and count-fetchers). 7 | /// A window is intended to be a much smaller section of the backend. It is specified by an offset and a size. 8 | /// Outwards it looks like a small list which contains only a few items of the whole backend. However, the sliding functionality 9 | /// makes it possible to go through the whole backend. 10 | /// 11 | public interface ISlidingWindow : 12 | IVirtualizationBase 13 | { 14 | /// 15 | /// Current offset of the window inside of the range of the items from the backend. The Offset marks the first item of the backend which is represented in the sliding window. 16 | /// 17 | int Offset { get; } 18 | 19 | /// 20 | /// Current maximum possible offset. Depends on the count of all backend items and the size of the window. 21 | /// 22 | int MaximumOffset { get; } 23 | 24 | /// 25 | /// Slides the window () to the backend one step to the start (left). 26 | /// 27 | void SlideLeft(); 28 | 29 | /// 30 | /// Slides the window () to the backend one step to the end (right). 31 | /// 32 | void SlideRight(); 33 | 34 | /// 35 | /// Sets the first entry of the window () to the given index of the backend. 36 | /// 37 | void JumpTo(int index); 38 | 39 | /// 40 | /// Increases windows size by one. 41 | /// 42 | void IncreaseWindowSize(); 43 | 44 | /// 45 | /// Decreases windows size by one. 46 | /// 47 | void DecreaseWindowSize(); 48 | 49 | /// 50 | /// Increases windows size by given increment. 51 | /// 52 | void IncreaseWindowSizeBy(int sizeIncrement); 53 | 54 | /// 55 | /// Decreases windows size by given increment. 56 | /// 57 | void DecreaseWindowSizeBy(int sizeIncrement); 58 | 59 | /// 60 | /// Sets windows size to given size. 61 | /// 62 | void SetWindowSizeTo(int size); 63 | } 64 | 65 | 66 | // ReSharper disable once PossibleInterfaceMemberAmbiguity 67 | // Ambiguous Members should be implemented explicitly 68 | /// 69 | /// Defines a generic window to the backend (accessed by the page- and count-fetchers). 70 | /// A window is intended to be a much smaller section of the backend. It is specified by an offset and a size. 71 | /// Outwards it looks like a small list which contains only a few items of the whole backend. However, the sliding functionality 72 | /// makes it possible to go through the whole backend. 73 | /// 74 | /// Item type. 75 | public interface ISlidingWindow : 76 | IVirtualizationBase, 77 | ISlidingWindow 78 | { 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/Utilities/DateTimeTimestampProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BFF.DataVirtualizingCollection.Utilities 4 | { 5 | internal class DateTimeTimestampProvider : ITimestampProvider 6 | { 7 | public DateTime Now => DateTime.Now; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/Utilities/ErrorMessage.cs: -------------------------------------------------------------------------------- 1 | namespace BFF.DataVirtualizingCollection.Utilities 2 | { 3 | internal static class ErrorMessage 4 | { 5 | internal static string ImpossibleExceptionMessage => 6 | "Unfortunately, an error occured during data virtualization, which should have been prevented. Please open an issue on https://github.com/Yeah69/BFF.DataVirtualizingCollection."; 7 | } 8 | } -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/Utilities/ITimestampProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BFF.DataVirtualizingCollection.Utilities 4 | { 5 | internal interface ITimestampProvider 6 | { 7 | DateTime Now { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BFF.DataVirtualizingCollection/VirtualizationBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.Specialized; 5 | using System.ComponentModel; 6 | using System.Reactive.Disposables; 7 | using System.Runtime.CompilerServices; 8 | using System.Threading.Tasks; 9 | 10 | namespace BFF.DataVirtualizingCollection 11 | { 12 | internal abstract class VirtualizationBase : 13 | IVirtualizationBase 14 | { 15 | int ICollection.Count => GetCountInner(); 16 | 17 | public bool IsSynchronized => false; 18 | 19 | public object SyncRoot { get; } = new(); 20 | 21 | public abstract int Count { get; } 22 | 23 | protected readonly CompositeDisposable CompositeDisposable = new(); 24 | 25 | int ICollection.Count => GetCountInner(); 26 | 27 | int IReadOnlyCollection.Count => GetCountInner(); 28 | 29 | bool ICollection.IsReadOnly => true; 30 | object? IList.this[int index] 31 | { 32 | get => IndexerInnerGet(index); 33 | set => throw new NotSupportedException(); 34 | } 35 | 36 | protected abstract T IndexerInnerGet(int index); 37 | 38 | public bool IsReadOnly => true; 39 | 40 | public T this[int index] 41 | { 42 | get => IndexerInnerGet(index); 43 | set => throw new NotSupportedException(); 44 | } 45 | 46 | private int GetCountInner() => Count; 47 | 48 | public IEnumerator GetEnumerator() 49 | { 50 | return Iterate().GetEnumerator(); 51 | 52 | IEnumerable Iterate() 53 | { 54 | for (var i = 0; i < Count; i++) 55 | yield return GetItemForEnumerator(i); 56 | } 57 | } 58 | 59 | protected abstract T GetItemForEnumerator(int i); 60 | 61 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 62 | 63 | private static int IndexOfInner() => -1; 64 | 65 | public int IndexOf(T item) => IndexOfInner(); 66 | 67 | private static bool ContainsInner() => IndexOfInner() != -1; 68 | 69 | public bool Contains(T item) => ContainsInner(); 70 | 71 | public bool IsFixedSize => true; 72 | 73 | public bool Contains(object value) => ContainsInner(); 74 | 75 | public int IndexOf(object value) => IndexOf((T) value); 76 | 77 | public abstract void Reset(); 78 | 79 | public event NotifyCollectionChangedEventHandler? CollectionChanged; 80 | public event PropertyChangedEventHandler? PropertyChanged; 81 | 82 | protected void OnCollectionChangedReplace(T newItem, T oldItem, int index) => 83 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, index)); 84 | 85 | protected virtual void OnCollectionChangedReset() => 86 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 87 | 88 | protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) => 89 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 90 | 91 | // ReSharper disable once ExplicitCallerInfoArgument 92 | protected void OnIndexerChanged() => OnPropertyChanged("Item[]"); 93 | 94 | public abstract Task InitializationCompleted { get; } 95 | public abstract int SelectedIndex { get; set; } 96 | 97 | #region NotSupported 98 | 99 | public void CopyTo(Array array, int index) => throw new NotSupportedException(); 100 | 101 | public void Add(T item) => throw new NotSupportedException(); 102 | 103 | public void Insert(int index, T item) => throw new NotSupportedException(); 104 | 105 | public bool Remove(T item) => throw new NotSupportedException(); 106 | 107 | public void Remove(object value) => throw new NotSupportedException(); 108 | 109 | public void RemoveAt(int index) => throw new NotSupportedException(); 110 | 111 | void IList.RemoveAt(int index) => throw new NotSupportedException(); 112 | 113 | public int Add(object value) => throw new NotSupportedException(); 114 | 115 | public void Clear() => throw new NotSupportedException(); 116 | 117 | public void Insert(int index, object value) => throw new NotSupportedException(); 118 | 119 | void ICollection.Clear() => throw new NotSupportedException(); 120 | 121 | public void CopyTo(T[] array, int arrayIndex) => throw new NotSupportedException(); 122 | 123 | #endregion 124 | 125 | public virtual async ValueTask DisposeAsync() 126 | { 127 | try 128 | { 129 | await InitializationCompleted.ConfigureAwait(false); 130 | } 131 | catch (OperationCanceledException) 132 | { 133 | // Ignore cancellation 134 | } 135 | CompositeDisposable.Dispose(); 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | v 4 | $(MinVerVersion) 5 | Dimitri Enns 6 | true 7 | 9 8 | enable 9 | nullable 10 | 11 | -------------------------------------------------------------------------------- /Documentation/Diagrams/Context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Documentation/Diagrams/Context.png -------------------------------------------------------------------------------- /Documentation/Diagrams/Context.uxf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 4 | 5 | UMLClass 6 | 7 | 540 8 | 300 9 | 200 10 | 30 11 | 12 | BFF.DataVirtulizingCollection 13 | 14 | 15 | 16 | UMLClass 17 | 18 | 540 19 | 220 20 | 200 21 | 30 22 | 23 | UI controls 24 | 25 | 26 | 27 | Relation 28 | 29 | 630 30 | 240 31 | 80 32 | 80 33 | 34 | lt=<-> 35 | Binding 36 | 10.0;10.0;10.0;60.0 37 | 38 | 39 | UMLClass 40 | 41 | 540 42 | 390 43 | 200 44 | 30 45 | 46 | Backend 47 | 48 | 49 | 50 | Relation 51 | 52 | 630 53 | 320 54 | 120 55 | 90 56 | 57 | lt=<- 58 | Page && 59 | Count Fechter 60 | 10.0;70.0;10.0;10.0 61 | 62 | 63 | -------------------------------------------------------------------------------- /Documentation/Diagrams/DataVirtualizingCollection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Documentation/Diagrams/DataVirtualizingCollection.png -------------------------------------------------------------------------------- /Documentation/Diagrams/PublicInterfaceHierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Documentation/Diagrams/PublicInterfaceHierarchy.png -------------------------------------------------------------------------------- /Documentation/Diagrams/PublicInterfaceHierarchy.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | From .Net (members hidden)From BFF.DataVirtualizingCollection«Interface»IBlue«Interface»IPurpleKey 63 | -------------------------------------------------------------------------------- /Documentation/Diagrams/SlidingWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Documentation/Diagrams/SlidingWindow.png -------------------------------------------------------------------------------- /Icon/Yeah69Logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Icon/Yeah69Logo.ico -------------------------------------------------------------------------------- /Icon/Yeah69Logo_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Icon/Yeah69Logo_256.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # BFF.DataVirtualizingCollection 3 | This is an approach to data-virtualizing collections intended to be used in WPF projects. 4 | 5 | ## Nuget package 6 | 7 | Nuget is the easiest way to install it (see [nuget page](https://www.nuget.org/packages/BFF.DataVirtualizingCollection/)): 8 | 9 | ``` 10 | > Install-Package BFF.DataVirtualizingCollection 11 | ``` 12 | 13 | ## Wiki 14 | 15 | A detailed documentation is available in the [Wiki](https://github.com/Yeah69/BFF.DataVirtualizingCollection/wiki). 16 | -------------------------------------------------------------------------------- /Sample.GettingStarted/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | -------------------------------------------------------------------------------- /Sample.GettingStarted/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace BFF.DataVirtualizingCollection.Sample.GettingStarted; 2 | 3 | public partial class App 4 | { 5 | } -------------------------------------------------------------------------------- /Sample.GettingStarted/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, 5 | ResourceDictionaryLocation.SourceAssembly 6 | )] -------------------------------------------------------------------------------- /Sample.GettingStarted/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | -------------------------------------------------------------------------------- /Sample.GettingStarted/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Concurrency; 4 | using System.Threading; 5 | using System.Windows; 6 | using System.Windows.Threading; 7 | using BFF.DataVirtualizingCollection.DataVirtualizingCollection; 8 | 9 | namespace BFF.DataVirtualizingCollection.Sample.GettingStarted; 10 | 11 | /// 12 | /// Interaction logic for MainWindow.xaml 13 | /// 14 | public partial class MainWindow 15 | { 16 | public MainWindow() 17 | { 18 | InitializeComponent(); 19 | 20 | // Welcome to the "Getting Started" sample 21 | 22 | // It is intentionally held as minimalistic as possible. It should be a quick starting point. 23 | // In this sample we want to virtualize the numbers from 0 to 2,147,483,646 (int.MaxValue - 1) 24 | 25 | // At minimum three settings are required from the user (you) of the data vitualizing collection 26 | // The notification scheduler, the page fetcher and the count fetcher 27 | 28 | // For WPF applications the notification scheduler should be based on the current applications dispatcher 29 | // That'll lead to the notifications (INotifyPropertyChanged & INotifyCollectionChanged) to being marshalled to the UI thread 30 | var notificationScheduler = 31 | new SynchronizationContextScheduler( 32 | new DispatcherSynchronizationContext( 33 | Application.Current.Dispatcher)); 34 | 35 | // Because we would like to virtualize the numbers from 0 to 2,147,483,646 (int.MaxValue - 1), 36 | // … the page fetcher will generate an array of consecutive numbers starting with the offset and of same count as the requested page size and 37 | Func pageFetcher = (offset, pageSize, _) => 38 | Enumerable.Range(offset, pageSize).ToArray(); 39 | // … the count just needs to return int.MaxValue 40 | Func countFetcher = _ => 41 | int.MaxValue; 42 | 43 | // Given those pre-requirements, we'll create a data virtualizing collections with the most basic options (like hoarding) 44 | var dataVirtualizingCollection = DataVirtualizingCollectionBuilder 45 | .Build(notificationScheduler) 46 | .NonPreloading() // neighboring pages are not preloaded 47 | .Hoarding() // once loaded pages are not removed for the lifetime of the data virtualizing collection 48 | .NonTaskBasedFetchers(pageFetcher, countFetcher) // synchronous fetchers 49 | .SyncIndexAccess(); // synchronous indexer (of IReadOnlyList) access (i.e. UI will block for not yet loaded pages) 50 | 51 | // And assign it to the ItemsSource dependency property of a ListBox 52 | // (in an MVVM application you actually would bind it to the property) 53 | List.ItemsSource = dataVirtualizingCollection; 54 | 55 | // That's it. We've virtualized some numbers (up until the billions, that is :D) 56 | 57 | // Please see this as a starting point. Depending of what you would like to do with the data virtualizing collection you can go different ways from here on 58 | 59 | // For example, you could replace virtualizing consecutive number with an other source, for example a database. 60 | 61 | // For beginners it might be of interest to dive deeper into this minimalistic example. You could put a breakpoint to the page and count fetchers code in order to explore when and how often they get called 62 | 63 | // You could also explore options deviating from these basic options (like preloading or least recently used page removal). 64 | // In that case, I'll also recommend to have a look into the UI of the sample project "Sample.View". It is like a playground for exploring the different options 65 | 66 | // This project has a very different kind of virtualization called the "sliding window". 67 | // It is meant for the special use case where a range of element of rather fixed size slides through the virtualized data. 68 | // If that matches your use case, then the sliding window might be right for you. 69 | } 70 | } -------------------------------------------------------------------------------- /Sample.GettingStarted/Sample.GettingStarted.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0-windows 6 | enable 7 | true 8 | BFF.DataVirtualizingCollection.Sample.GettingStarted 9 | BFF.DataVirtualizingCollection.Sample.GettingStarted 10 | 10 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Sample.Model/AutofacModule.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Autofac; 3 | using Module = Autofac.Module; 4 | 5 | namespace BFF.DataVirtualizingCollection.Sample.Model 6 | { 7 | public class AutofacModule : Module 8 | { 9 | protected override void Load(ContainerBuilder builder) 10 | { 11 | var assemblies = new[] 12 | { 13 | Assembly.GetExecutingAssembly() 14 | }; 15 | 16 | builder.RegisterAssemblyTypes(assemblies) 17 | .AsImplementedInterfaces() 18 | .AsSelf(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Sample.Model/BackendAccesses/AllNumbersFakeBackendAccess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace BFF.DataVirtualizingCollection.Sample.Model.BackendAccesses 5 | { 6 | public interface IAllNumbersFakeBackendAccess : IBackendAccess 7 | { 8 | } 9 | 10 | internal class AllNumbersFakeBackendAccess : IAllNumbersFakeBackendAccess 11 | { 12 | public string Name => "All Positive Numbers"; 13 | 14 | public int[] PageFetch(int pageOffset, int pageSize) => Enumerable.Range(pageOffset, pageSize).ToArray(); 15 | 16 | public int PlaceholderFetch(int _, int __) 17 | { 18 | return -11; 19 | } 20 | 21 | public int PreloadingPlaceholderFetch(int _, int __) 22 | { 23 | return -21; 24 | } 25 | 26 | public int CountFetch() 27 | { 28 | return int.MaxValue; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Sample.Model/BackendAccesses/HighWorkloadFakeBackendAccess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using BFF.DataVirtualizingCollection.Sample.Model.Models; 4 | 5 | namespace BFF.DataVirtualizingCollection.Sample.Model.BackendAccesses 6 | { 7 | public interface IHighWorkloadFakeBackendAccess : IBackendAccess 8 | { 9 | } 10 | 11 | internal class HighWorkloadFakeBackendAccess : IHighWorkloadFakeBackendAccess 12 | { 13 | private ISomeWorkloadObject Placeholder { get; } = (ISomeWorkloadObject) new LowWorkloadObject(-11); 14 | 15 | private ISomeWorkloadObject PreloadingPlaceholder { get; } = (ISomeWorkloadObject) new LowWorkloadObject(-11); 16 | 17 | public string Name => "High Workload Simulation"; 18 | 19 | public ISomeWorkloadObject[] PageFetch(int pageOffset, int pageSize) => 20 | Enumerable 21 | .Range(pageOffset, pageSize) 22 | .Select(i => (ISomeWorkloadObject) new HighWorkloadObject(i)) 23 | .ToArray(); 24 | 25 | public ISomeWorkloadObject PlaceholderFetch(int _, int __) 26 | { 27 | return Placeholder; 28 | } 29 | 30 | public ISomeWorkloadObject PreloadingPlaceholderFetch(int _, int __) 31 | { 32 | return PreloadingPlaceholder; 33 | } 34 | 35 | public int CountFetch() 36 | { 37 | return int.MaxValue; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Sample.Model/BackendAccesses/IBackendAccess.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace BFF.DataVirtualizingCollection.Sample.Model.BackendAccesses 5 | { 6 | public interface IBackendAccess 7 | { 8 | string Name { get; } 9 | 10 | T[] PageFetch(int pageOffset, int pageSize); 11 | 12 | async IAsyncEnumerable AsyncEnumerablePageFetch(int pageOffset, int pageSize) 13 | { 14 | foreach (var item in PageFetch(pageOffset, pageSize)) 15 | { 16 | await Task.Delay(1); 17 | yield return item; 18 | } 19 | } 20 | 21 | T PlaceholderFetch(int pageOffset, int indexInsidePage); 22 | 23 | T PreloadingPlaceholderFetch(int pageOffset, int indexInsidePage); 24 | 25 | int CountFetch(); 26 | } 27 | } -------------------------------------------------------------------------------- /Sample.Model/BackendAccesses/MillionNumbersBackendAccess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BFF.DataVirtualizingCollection.Sample.Model.PersistenceLink; 3 | 4 | namespace BFF.DataVirtualizingCollection.Sample.Model.BackendAccesses 5 | { 6 | public interface IMillionNumbersBackendAccess : IBackendAccess 7 | { 8 | } 9 | 10 | internal class MillionNumbersBackendAccess : IMillionNumbersBackendAccess 11 | { 12 | private readonly IFetchMillionNumbersFromBackend _fetchMillionNumbersFromBackend; 13 | 14 | public MillionNumbersBackendAccess( 15 | IFetchMillionNumbersFromBackend fetchMillionNumbersFromBackend) 16 | { 17 | _fetchMillionNumbersFromBackend = fetchMillionNumbersFromBackend; 18 | } 19 | 20 | public string Name => "A Million Numbers Accessed Through Sqlite"; 21 | 22 | public long[] PageFetch(int pageOffset, int pageSize) => _fetchMillionNumbersFromBackend.FetchPage(pageOffset, pageSize); 23 | 24 | public long PlaceholderFetch(int _, int __) 25 | { 26 | return -11L; 27 | } 28 | 29 | public long PreloadingPlaceholderFetch(int _, int __) 30 | { 31 | return -21L; 32 | } 33 | 34 | public int CountFetch() 35 | { 36 | return _fetchMillionNumbersFromBackend.CountFetch(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Sample.Model/BackendAccesses/ProfilesFakeBackendAccess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using BFF.DataVirtualizingCollection.Sample.Model.Models; 4 | 5 | namespace BFF.DataVirtualizingCollection.Sample.Model.BackendAccesses 6 | { 7 | public interface IProfilesFakeBackendAccess : IBackendAccess 8 | { 9 | } 10 | 11 | internal class ProfilesFakeBackendAccess : IProfilesFakeBackendAccess 12 | { 13 | 14 | public string Name => "Profiles"; 15 | 16 | public IProfile[] PageFetch(int pageOffset, int pageSize) => 17 | Enumerable 18 | .Range(pageOffset, pageSize) 19 | .Select(i => ProfileStatic.ProfilePool[i % ProfileStatic.ProfilePool.Count]) 20 | .ToArray(); 21 | 22 | public IProfile PlaceholderFetch(int _, int __) 23 | { 24 | return ProfileStatic.Empty; 25 | } 26 | 27 | public IProfile PreloadingPlaceholderFetch(int pageOffset, int indexInsidePage) 28 | { 29 | return ProfileStatic.Preloading; 30 | } 31 | 32 | public int CountFetch() 33 | { 34 | return 420420; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Sample.Model/Models/HighWorkloadObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BFF.DataVirtualizingCollection.Sample.Model.Models 4 | { 5 | public interface IHighWorkloadObject : ISomeWorkloadObject 6 | { 7 | } 8 | 9 | internal class HighWorkloadObject : SomeWorkloadObject, IHighWorkloadObject, IDisposable 10 | { 11 | // Simulates workload 12 | // ReSharper disable once UnusedMember.Local 13 | private readonly byte[] _workload = new byte[12500]; 14 | 15 | public HighWorkloadObject(int number) : base(number) 16 | { 17 | } 18 | 19 | public void Dispose() 20 | { 21 | Console.WriteLine("Disposed"); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Sample.Model/Models/LowWorkloadObject.cs: -------------------------------------------------------------------------------- 1 | namespace BFF.DataVirtualizingCollection.Sample.Model.Models 2 | { 3 | public interface ILowWorkloadObject : ISomeWorkloadObject 4 | { 5 | } 6 | 7 | internal class LowWorkloadObject : SomeWorkloadObject, ILowWorkloadObject 8 | { 9 | public LowWorkloadObject(int number) : base(number) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Sample.Model/Models/SomeWorkloadObject.cs: -------------------------------------------------------------------------------- 1 | namespace BFF.DataVirtualizingCollection.Sample.Model.Models 2 | { 3 | public interface ISomeWorkloadObject 4 | { 5 | int Number { get; } 6 | } 7 | 8 | internal abstract class SomeWorkloadObject : ISomeWorkloadObject 9 | { 10 | public SomeWorkloadObject(int number) 11 | { 12 | Number = number; 13 | } 14 | 15 | public int Number { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /Sample.Model/PersistenceLink/IFetchMillionNumbersFromBackend.cs: -------------------------------------------------------------------------------- 1 | namespace BFF.DataVirtualizingCollection.Sample.Model.PersistenceLink 2 | { 3 | public interface IFetchMillionNumbersFromBackend 4 | { 5 | long[] FetchPage(int pageOffset, int pageSize); 6 | 7 | int CountFetch(); 8 | } 9 | } -------------------------------------------------------------------------------- /Sample.Model/Sample.Model.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | BFF.DataVirtualizingCollection.Sample.Model 6 | BFF.DataVirtualizingCollection.Sample.Model 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Sample.Persistence.Proxy/AutofacModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Module = Autofac.Module; 3 | 4 | namespace BFF.DataVirtualizingCollection.Sample.Persistence.Proxy 5 | { 6 | public class AutofacModule : Module 7 | { 8 | protected override void Load(ContainerBuilder builder) 9 | { 10 | builder.RegisterModule(new BFF.DataVirtualizingCollection.Sample.Persistence.AutofacModule()); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Sample.Persistence.Proxy/Sample.Persistence.Proxy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | BFF.DataVirtualizingCollection.Sample.Persistence.Proxy 6 | BFF.DataVirtualizingCollection.Sample.Persistence.Proxy 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sample.Persistence/AutofacModule.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Autofac; 3 | using Module = Autofac.Module; 4 | 5 | namespace BFF.DataVirtualizingCollection.Sample.Persistence 6 | { 7 | public class AutofacModule : Module 8 | { 9 | protected override void Load(ContainerBuilder builder) 10 | { 11 | var assemblies = new[] 12 | { 13 | Assembly.GetExecutingAssembly() 14 | }; 15 | 16 | builder.RegisterAssemblyTypes(assemblies) 17 | .AsImplementedInterfaces() 18 | .AsSelf(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Sample.Persistence/Databases/BFF.DataVirtualizingCollection.MillionNumbers.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.Persistence/Databases/BFF.DataVirtualizingCollection.MillionNumbers.sqlite -------------------------------------------------------------------------------- /Sample.Persistence/PersistenceLink/FetchMillionNumbersFromBackend.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using BFF.DataVirtualizingCollection.Sample.Model.PersistenceLink; 5 | using Microsoft.Data.Sqlite; 6 | 7 | namespace BFF.DataVirtualizingCollection.Sample.Persistence.PersistenceLink 8 | { 9 | internal class FetchMillionNumbersFromBackend : IFetchMillionNumbersFromBackend 10 | { 11 | private static readonly string PathToSimpleTestDb = $"{System.Reflection.Assembly.GetEntryAssembly()?.Location.Remove(System.Reflection.Assembly.GetEntryAssembly()?.Location.LastIndexOf(Path.DirectorySeparatorChar) ?? 0)}{Path.DirectorySeparatorChar}Databases{Path.DirectorySeparatorChar}BFF.DataVirtualizingCollection.MillionNumbers.sqlite"; 12 | public long[] FetchPage(int pageOffset, int pageSize) 13 | { 14 | using var sqliteConnection = 15 | new SqliteConnection( 16 | new SqliteConnectionStringBuilder 17 | { 18 | DataSource = PathToSimpleTestDb 19 | } 20 | .ToString()); 21 | sqliteConnection.Open(); 22 | sqliteConnection.BeginTransaction(); 23 | var sqliteCommand = sqliteConnection.CreateCommand(); 24 | sqliteCommand.CommandText = $"SELECT Number FROM Numbers LIMIT {pageSize} OFFSET {pageOffset};"; 25 | var sqliteDataReader = sqliteCommand.ExecuteReader(); 26 | IList ret = new List(); 27 | while (sqliteDataReader.Read()) 28 | ret.Add((long)sqliteDataReader["Number"]); 29 | return ret.ToArray(); 30 | } 31 | 32 | public int CountFetch() 33 | { 34 | using var sqliteConnection = 35 | new SqliteConnection( 36 | new SqliteConnectionStringBuilder 37 | { 38 | DataSource = PathToSimpleTestDb 39 | } 40 | .ToString()); 41 | sqliteConnection.Open(); 42 | var sqliteCommand = sqliteConnection.CreateCommand(); 43 | sqliteCommand.CommandText = "SELECT Count(*) FROM Numbers;"; 44 | return (int)(long)sqliteCommand.ExecuteScalar(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Sample.Persistence/Sample.Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | BFF.DataVirtualizingCollection.Sample.Persistence 6 | BFF.DataVirtualizingCollection.Sample.Persistence 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sample.View/App.xaml: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /Sample.View/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace BFF.DataVirtualizingCollection.Sample.View 4 | { 5 | public partial class App 6 | { 7 | public App() 8 | { 9 | var mainWindowView = AutofacModule.Start(); 10 | mainWindowView.Show(); 11 | } 12 | 13 | public static Visibility IsDebug 14 | { 15 | #if DEBUG 16 | get { return Visibility.Visible; } 17 | #else 18 | get { return Visibility.Collapsed; } 19 | #endif 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Sample.View/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] -------------------------------------------------------------------------------- /Sample.View/AutofacModule.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Disposables; 2 | using System.Reflection; 3 | using Autofac; 4 | using BFF.DataVirtualizingCollection.Sample.View.ViewModelInterfaceImplementations; 5 | using BFF.DataVirtualizingCollection.Sample.View.Views; 6 | using Module = Autofac.Module; 7 | 8 | namespace BFF.DataVirtualizingCollection.Sample.View 9 | { 10 | public class AutofacModule : Module 11 | { 12 | public static MainWindowView Start() 13 | { 14 | var builder = new ContainerBuilder(); 15 | builder.RegisterModule(new AutofacModule()); 16 | return builder 17 | .Build() 18 | .BeginLifetimeScope() 19 | .Resolve(); 20 | } 21 | 22 | private AutofacModule() 23 | {} 24 | 25 | protected override void Load(ContainerBuilder builder) 26 | { 27 | var assemblies = new[] 28 | { 29 | Assembly.GetExecutingAssembly() 30 | }; 31 | 32 | builder.RegisterAssemblyTypes(assemblies) 33 | .AsImplementedInterfaces() 34 | .AsSelf(); 35 | 36 | builder.RegisterType() 37 | .AsImplementedInterfaces() 38 | .AsSelf() 39 | .SingleInstance(); 40 | 41 | builder.RegisterType() 42 | .AsSelf() 43 | .UsingConstructor(() => new CompositeDisposable()) 44 | .InstancePerLifetimeScope(); 45 | 46 | builder.RegisterModule(new BFF.DataVirtualizingCollection.Sample.ViewModel.AutofacModule()); 47 | 48 | builder.RegisterModule(new BFF.DataVirtualizingCollection.Sample.Persistence.Proxy.AutofacModule()); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Sample.View/ProfilePics/00_Wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/00_Wide.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/01_Paria.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/01_Paria.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/02_Morexandra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/02_Morexandra.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/03_Smennifer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/03_Smennifer.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/04_Anyetlana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/04_Anyetlana.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/05_Korko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/05_Korko.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/06_Kowel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/06_Kowel.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/07_Sinia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/07_Sinia.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/08_Wonathan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/08_Wonathan.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/09_Matban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/09_Matban.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/10_Surgiana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/10_Surgiana.png -------------------------------------------------------------------------------- /Sample.View/ProfilePics/11_Jogory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yeah69/BFF.DataVirtualizingCollection/6001b74136ffd77c7d54e4b4c7f161613dd88974/Sample.View/ProfilePics/11_Jogory.png -------------------------------------------------------------------------------- /Sample.View/Sample.View.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | netcoreapp3.1 6 | true 7 | BFF.DataVirtualizingCollection.Sample.View 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Always 32 | 33 | 34 | Always 35 | 36 | 37 | Always 38 | 39 | 40 | Always 41 | 42 | 43 | Always 44 | 45 | 46 | Always 47 | 48 | 49 | Always 50 | 51 | 52 | Always 53 | 54 | 55 | Always 56 | 57 | 58 | Always 59 | 60 | 61 | Always 62 | 63 | 64 | Always 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | GeneralOptionsView.xaml 86 | 87 | 88 | SlidingWindowOptionsViewModel.xaml 89 | 90 | 91 | GeneralFunctionsView.xaml 92 | 93 | 94 | SpecificFunctionsView.xaml 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Sample.View/Utilities/Converters.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Data; 3 | 4 | namespace BFF.DataVirtualizingCollection.Sample.View.Utilities 5 | { 6 | public static class Converters 7 | { 8 | public static IValueConverter BoolToVisibility = 9 | LambdaConverters.ValueConverter.Create( 10 | e => e.Value ? Visibility.Visible : Visibility.Collapsed); 11 | 12 | public static IValueConverter ValueEqualsToParameter = 13 | LambdaConverters.ValueConverter.Create( 14 | e => Equals(e.Value, e.Parameter)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sample.View/Utilities/DataTemplateSelectors.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using BFF.DataVirtualizingCollection.Sample.ViewModel.Adapters; 4 | using BFF.DataVirtualizingCollection.Sample.ViewModel.ViewModels; 5 | 6 | namespace BFF.DataVirtualizingCollection.Sample.View.Utilities 7 | { 8 | public class BackendAccessDataTemplateSelector : DataTemplateSelector 9 | { 10 | public DataTemplate? AllNumbersTemplate { get; set; } 11 | public DataTemplate? HighWorkloadTemplate { get; set; } 12 | public DataTemplate? MillionNumbersTemplate { get; set; } 13 | public DataTemplate? ProfileTemplate { get; set; } 14 | 15 | public override DataTemplate? SelectTemplate(object item, DependencyObject container) 16 | { 17 | return item is IDataVirtualizingCollectionViewModelBase dataVirtualizingCollectionViewModel 18 | ? dataVirtualizingCollectionViewModel.BackendAccessKind switch 19 | { 20 | BackendAccessKind.AllNumbers => AllNumbersTemplate, 21 | BackendAccessKind.HighWorkload => HighWorkloadTemplate, 22 | BackendAccessKind.MillionNumbers => MillionNumbersTemplate, 23 | BackendAccessKind.Profiles => ProfileTemplate, 24 | _ => base.SelectTemplate(item, container) 25 | } 26 | : base.SelectTemplate(item, container); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Sample.View/Utilities/ProfileViewStatic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Data; 3 | using System.Windows.Media; 4 | using System.Windows.Media.Imaging; 5 | using BFF.DataVirtualizingCollection.Sample.ViewModel.ViewModels; 6 | using MahApps.Metro.IconPacks; 7 | 8 | namespace BFF.DataVirtualizingCollection.Sample.View.Utilities 9 | { 10 | public static class ProfileViewStatic 11 | { 12 | public static IValueConverter ToCompanyBrush = 13 | LambdaConverters.ValueConverter.Create( 14 | e => e.Value.IsFreelancer ? Brushes.Green : Brushes.Blue); 15 | 16 | public static IValueConverter ToCompanyText = 17 | LambdaConverters.ValueConverter.Create( 18 | e => e.Value.IsFreelancer ? "Freelancer" : e.Value.CompanyName ?? string.Empty); 19 | 20 | public static IValueConverter ToCompanyIcon = 21 | LambdaConverters.ValueConverter.Create( 22 | e => e.Value.IsFreelancer ? PackIconMaterialKind.AccountOutline : PackIconMaterialKind.City); 23 | 24 | public static IValueConverter ToImageSource = 25 | LambdaConverters.ValueConverter.Create( 26 | e => new BitmapImage(new Uri(e.Value.PicturePath))); 27 | 28 | public static IValueConverter PrefixedHiddenAbilitiesCount = 29 | LambdaConverters.ValueConverter.Create( 30 | e => $"+{e.Value}"); 31 | 32 | public static IValueConverter ProfilesTitle = 33 | LambdaConverters.ValueConverter.Create( 34 | e => $"Profiles ({e.Value})"); 35 | } 36 | } -------------------------------------------------------------------------------- /Sample.View/ViewModelInterfaceImplementations/GetScheduler.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Concurrency; 2 | using System.Windows; 3 | using BFF.DataVirtualizingCollection.Sample.ViewModel.Interfaces; 4 | 5 | namespace BFF.DataVirtualizingCollection.Sample.View.ViewModelInterfaceImplementations 6 | { 7 | internal class GetSchedulers : IGetSchedulers 8 | { 9 | public GetSchedulers() 10 | { 11 | NotificationScheduler = new DispatcherScheduler(Application.Current.Dispatcher); 12 | } 13 | 14 | public IScheduler NotificationScheduler { get; } 15 | public IScheduler BackgroundScheduler => TaskPoolScheduler.Default; 16 | } 17 | } -------------------------------------------------------------------------------- /Sample.View/Views/DataVirtualizingCollectionView.xaml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |