{"id":21730148,"url":"https://github.com/nventive/chinook.dataloader","last_synced_at":"2025-04-13T00:14:54.286Z","repository":{"id":39789844,"uuid":"269186145","full_name":"nventive/Chinook.DataLoader","owner":"nventive","description":"Display data from an asynchronous source","archived":false,"fork":false,"pushed_at":"2025-04-02T15:54:51.000Z","size":317,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-04-13T00:14:45.951Z","etag":null,"topics":["android","chinook","data-loader","dataloader","dotnet","ios","mobile","mvvm","uno-platform","uwp"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nventive.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-03T20:19:27.000Z","updated_at":"2025-04-02T15:54:54.000Z","dependencies_parsed_at":"2023-12-22T18:31:08.081Z","dependency_job_id":"ae79bc3f-2de6-4ba2-9987-cdf0da45ac1b","html_url":"https://github.com/nventive/Chinook.DataLoader","commit_stats":{"total_commits":72,"total_committers":17,"mean_commits":4.235294117647059,"dds":0.7638888888888888,"last_synced_commit":"f9a995f1649df926d6e9200eda618b0f73056a5a"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":"nventive/Template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nventive%2FChinook.DataLoader","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nventive%2FChinook.DataLoader/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nventive%2FChinook.DataLoader/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nventive%2FChinook.DataLoader/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nventive","download_url":"https://codeload.github.com/nventive/Chinook.DataLoader/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248647274,"owners_count":21139086,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","chinook","data-loader","dataloader","dotnet","ios","mobile","mvvm","uno-platform","uwp"],"created_at":"2024-11-26T04:13:11.622Z","updated_at":"2025-04-13T00:14:54.260Z","avatar_url":"https://github.com/nventive.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# Chinook.DataLoader\n\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](LICENSE) ![Version](https://img.shields.io/nuget/v/Chinook.DataLoader.Abstractions?style=flat-square) ![Downloads](https://img.shields.io/nuget/dt/Chinook.DataLoader.Abstractions?style=flat-square)\n\nCustomizable async data loading recipes for MVVM development.\n\n![iOS-Gif](doc/DataLoader.iOS.gif)\n\n\u003e 🎥 Here we can see the **loading**, **error**, and **data** visual states.\n\n## Cornerstones\n\n- **Highly Extensible**\n  - Everything is interface-based to easily allow more implementations.\n  - A single framework can't cover everything.\n    Our architecture is designed in a way that allows you to integrate your favorites tools easily.\n- **Highly Flexible**\n  - \"Data loading\" is a broad topic.\n    We aim for our recipes to be flexible enough to support as many use-cases as possible.\n\n### More like this\nThe Chinook namespace has other recipes for .Net MVVM applications.\n- [Chinook.DynamicMvvm](https://github.com/nventive/Chinook.DynamicMvvm): MVVM libraries for extensible and declarative ViewModels.\n- [Chinook.Navigation](https://github.com/nventive/Chinook.Navigation): Navigators for ViewModel-first navigation.\n- [Chinook.BackButtonManager](https://github.com/nventive/Chinook.BackButtonManager): Abstractions to deal with hardware back buttons.\n\n## Getting Started\n\n1. Install the latest version of `Chinook.DataLoader.Uno` or `Chinook.DataLoader.Uno.WinUI` in your project.\n\n1. Add a `IDataLoader` to your ViewModel. \n\n   ```csharp\n   public MainPageViewModel()\n   {\n     MyAsyncValue = DataLoaderBuilder\u003cint\u003e\n       .Empty\n       .WithName(\"MyAsyncValue\")\n       .WithLoadMethod(LoadMyAsyncValue)\n       .Build();\n   }\n\n   public IDataLoader\u003cint\u003e MyAsyncValue { get; }\n\n   private async Task\u003cint\u003e LoadMyAsyncValue(CancellationToken ct, IDataLoaderRequest request)\n   {\n      // Your async operation here.\n      await Task.Delay(1000, ct);\n      return default(int);\n   }\n   ```\n   \u003e 💡 If you use [Chinook.DynamicMvvm](https://github.com/nventive/Chinook.DynamicMvvm), you can simplify the declaration.\n   \u003e More on that [here](#DynamicMvvm).\n\n1. Add a `DataLoaderView` style in your xaml.\n   \u003cdetails\u003e\n   \u003csummary\u003eExpand to see code sample.\u003c/summary\u003e\n   \u003cp\u003e\n\n   You can add the following `DataTemplate`s and `Style` to you `Page.Resources` or to a merged dictionary in you App.xaml.\n   Feel free to copy and modify this code.\n   ```xml\n   \u003cDataTemplate x:Key=\"DataLoaderViewEmptyTemplate\"\u003e\n        \u003cGrid\u003e\n            \u003cTextBlock Text=\"Nothing to show here\"\n                        HorizontalAlignment=\"Center\"\n                        VerticalAlignment=\"Top\"\n                        Margin=\"0,44,0,0\" /\u003e\n        \u003c/Grid\u003e\n    \u003c/DataTemplate\u003e\n\n    \u003cDataTemplate x:Key=\"DataLoaderViewErrorNotificationTemplate\"\u003e\n        \u003cGrid Background=\"#570000\"\n              VerticalAlignment=\"Bottom\"\n              Padding=\"8\"\u003e\n\n            \u003cGrid.ColumnDefinitions\u003e\n                \u003cColumnDefinition Width=\"*\" /\u003e\n                \u003cColumnDefinition Width=\"Auto\" /\u003e\n            \u003c/Grid.ColumnDefinitions\u003e\n\n            \u003cTextBlock Foreground=\"White\"\n                        HorizontalAlignment=\"Left\"\n                        VerticalAlignment=\"Center\"\u003e\n    \u003cRun Text=\"Something went wrong.\" /\u003e\n    \u003cRun Text=\"{Binding Error.Message}\" /\u003e\n            \u003c/TextBlock\u003e\n\n            \u003c!-- Try Again Button --\u003e\n            \u003cButton Content=\"Try Again\"\n                    Background=\"#990000\"\n                    HorizontalAlignment=\"Center\"\n                    Command=\"{Binding View.RefreshCommand}\"\n                    Grid.Column=\"1\" /\u003e\n        \u003c/Grid\u003e\n    \u003c/DataTemplate\u003e\n\n    \u003c!-- Error State --\u003e\n    \u003cDataTemplate x:Key=\"DataLoaderViewErrorTemplate\"\u003e\n        \u003cGrid\u003e\n            \u003cStackPanel VerticalAlignment=\"Center\"\n                        HorizontalAlignment=\"Stretch\"\u003e\n\n                \u003c!-- Title --\u003e\n                \u003cTextBlock Text=\"Something went wrong\"\n                            TextAlignment=\"Center\"\n                            Margin=\"0,0,0,8\" /\u003e\n\n                \u003c!-- Message --\u003e\n                \u003cTextBlock Text=\"Please try again.\"\n                            TextAlignment=\"Center\" /\u003e\n\n                \u003c!-- Try Again Button --\u003e\n                \u003cButton Content=\"Reload content\"\n                        HorizontalAlignment=\"Stretch\"\n                        Command=\"{Binding View.RefreshCommand}\"\n                        Margin=\"22,24,22,0\" /\u003e\n            \u003c/StackPanel\u003e\n        \u003c/Grid\u003e\n    \u003c/DataTemplate\u003e\n\n    \u003cStyle x:Key=\"DefaultDataLoaderViewStyle\"\n            TargetType=\"dl:DataLoaderView\"\u003e\n        \u003cSetter Property=\"IsTabStop\"\n                Value=\"False\" /\u003e\n        \u003cSetter Property=\"HorizontalAlignment\"\n                Value=\"Stretch\" /\u003e\n        \u003cSetter Property=\"VerticalAlignment\"\n                Value=\"Stretch\" /\u003e\n        \u003cSetter Property=\"HorizontalContentAlignment\"\n                Value=\"Stretch\" /\u003e\n        \u003cSetter Property=\"VerticalContentAlignment\"\n                Value=\"Stretch\" /\u003e\n\n        \u003cSetter Property=\"EmptyTemplate\"\n                Value=\"{StaticResource DataLoaderViewEmptyTemplate}\" /\u003e\n        \u003cSetter Property=\"ErrorTemplate\"\n                Value=\"{StaticResource DataLoaderViewErrorTemplate}\" /\u003e\n        \u003cSetter Property=\"ErrorNotificationTemplate\"\n                Value=\"{StaticResource DataLoaderViewErrorNotificationTemplate}\" /\u003e\n\n        \u003c!-- This represents the minimum duration of a visual state.\n    You should keep this high enough to prevent visual states from changing too fast.\n    This is the property that prevents flickers. --\u003e\n        \u003cSetter Property=\"StateMinimumDuration\"\n                Value=\"0:0:1.5\" /\u003e\n\n        \u003c!-- This represents the time before updating to a new state during which another update can happen.\n    You should keep this low enough to prevent slowing down your user experience.\n    This is the property that prevents seeing loading states that are not necessary. --\u003e\n        \u003cSetter Property=\"StateChangingThrottleDelay\"\n                Value=\"0:0:0.100\" /\u003e\n\n        \u003cSetter Property=\"Template\"\u003e\n            \u003cSetter.Value\u003e\n                \u003cControlTemplate TargetType=\"dl:DataLoaderView\"\u003e\n                    \u003cGrid\u003e\n                        \u003cVisualStateManager.VisualStateGroups\u003e\n\n                            \u003cVisualStateGroup x:Name=\"DataStates\"\u003e\n                                \u003cVisualState x:Name=\"Initial\" /\u003e\n                                \u003cVisualState x:Name=\"Data\"\u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"ContentPresenter.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n                                \u003cVisualState x:Name=\"Empty\" /\u003e\n                            \u003c/VisualStateGroup\u003e\n\n                            \u003cVisualStateGroup x:Name=\"ErrorStates\"\u003e\n                                \u003cVisualState x:Name=\"NoError\" /\u003e\n                                \u003cVisualState x:Name=\"Error\" /\u003e\n                            \u003c/VisualStateGroup\u003e\n\n                            \u003cVisualStateGroup x:Name=\"LoadingStates\"\u003e\n                                \u003cVisualState x:Name=\"NotLoading\" /\u003e\n                                \u003cVisualState x:Name=\"Loading\" \u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"LoadingIndicator.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                        \u003cSetter Target=\"LoadingIndicator.IsActive\"\n                                                Value=\"True\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n                            \u003c/VisualStateGroup\u003e\n\n                            \u003cVisualStateGroup x:Name=\"CombinedStates\"\u003e\n                                \u003cVisualState x:Name=\"Initial_NoError_NotLoading\" /\u003e\n                                \u003cVisualState x:Name=\"Initial_NoError_Loading\" /\u003e\n\n                                \u003cVisualState x:Name=\"Initial_Error_NotLoading\"\u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"ErrorPresenter.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n                                \u003cVisualState x:Name=\"Initial_Error_Loading\"\u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"ErrorPresenter.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n\n                                \u003cVisualState x:Name=\"Data_NoError_NotLoading\" /\u003e\n                                \u003cVisualState x:Name=\"Data_NoError_Loading\" /\u003e\n\n                                \u003cVisualState x:Name=\"Data_Error_NotLoading\"\u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"ErrorNotificationPresenter.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n                                \u003cVisualState x:Name=\"Data_Error_Loading\"\u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"ErrorNotificationPresenter.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n\n                                \u003cVisualState x:Name=\"Empty_NoError_NotLoading\"\u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"EmptyPresenter.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n                                \u003cVisualState x:Name=\"Empty_NoError_Loading\" /\u003e\n                                \u003cVisualState x:Name=\"Empty_Error_NotLoading\"\u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"ErrorNotificationPresenter.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n                                \u003cVisualState x:Name=\"Empty_Error_Loading\"\u003e\n                                    \u003cVisualState.Setters\u003e\n                                        \u003cSetter Target=\"ErrorNotificationPresenter.Visibility\"\n                                                Value=\"Visible\" /\u003e\n                                    \u003c/VisualState.Setters\u003e\n                                \u003c/VisualState\u003e\n                            \u003c/VisualStateGroup\u003e\n                        \u003c/VisualStateManager.VisualStateGroups\u003e\n\n                        \u003cContentPresenter x:Name=\"ContentPresenter\"\n                                          Content=\"{TemplateBinding State}\"\n                                          ContentTemplate=\"{TemplateBinding ContentTemplate}\"\n                                          HorizontalAlignment=\"{TemplateBinding HorizontalContentAlignment}\"\n                                          VerticalAlignment=\"{TemplateBinding VerticalContentAlignment}\"\n                                          Visibility=\"Collapsed\"\n                                          Padding=\"{TemplateBinding Padding}\" /\u003e\n\n                        \u003cContentPresenter x:Name=\"EmptyPresenter\"\n                                          Content=\"{TemplateBinding State}\"\n                                          ContentTemplate=\"{TemplateBinding EmptyTemplate}\"\n                                          HorizontalAlignment=\"{TemplateBinding HorizontalContentAlignment}\"\n                                          VerticalAlignment=\"{TemplateBinding VerticalContentAlignment}\"\n                                          Visibility=\"Collapsed\"\n                                          Padding=\"{TemplateBinding Padding}\" /\u003e\n\n                        \u003cContentPresenter x:Name=\"ErrorPresenter\"\n                                          Content=\"{TemplateBinding State}\"\n                                          ContentTemplate=\"{TemplateBinding ErrorTemplate}\"\n                                          HorizontalAlignment=\"{TemplateBinding HorizontalContentAlignment}\"\n                                          VerticalAlignment=\"{TemplateBinding VerticalContentAlignment}\"\n                                          Visibility=\"Collapsed\"\n                                          Padding=\"{TemplateBinding Padding}\" /\u003e\n\n                        \u003cContentPresenter x:Name=\"ErrorNotificationPresenter\"\n                                          Content=\"{TemplateBinding State}\"\n                                          ContentTemplate=\"{TemplateBinding ErrorNotificationTemplate}\"\n                                          HorizontalAlignment=\"{TemplateBinding HorizontalContentAlignment}\"\n                                          VerticalAlignment=\"{TemplateBinding VerticalContentAlignment}\"\n                                          Visibility=\"Collapsed\"\n                                          Padding=\"{TemplateBinding Padding}\" /\u003e\n\n                        \u003cProgressRing x:Name=\"LoadingIndicator\"\n                                      Foreground=\"Black\"\n                                      Visibility=\"Collapsed\"\n                                      IsActive=\"False\"\n                                      Width=\"20\"\n                                      Height=\"20\"\n                                      HorizontalAlignment=\"Center\"\n                                      VerticalAlignment=\"Center\" /\u003e\n                    \u003c/Grid\u003e\n                \u003c/ControlTemplate\u003e\n            \u003c/Setter.Value\u003e\n        \u003c/Setter\u003e\n    \u003c/Style\u003e\n   ```\n    \u003c/p\u003e\u003c/details\u003e\n1. Add a `DataLoaderView` in your xaml.\n\n   ```xml\n   \u003c!-- Add the DataLoaderView namespace: xmlns:dl=\"using:Chinook.DataLoader\" --\u003e\n   \u003cdl:DataLoaderView Source=\"{Binding MyAsyncValue}\"\n                      Style=\"{StaticResource DefaultDataLoaderViewStyle}\"\u003e\n     \u003cDataTemplate\u003e\n       \u003cStackPanel\u003e\n         \u003cTextBlock Text=\"{Binding Data}\" /\u003e\n       \u003c/StackPanel\u003e\n     \u003c/DataTemplate\u003e\n   \u003c/dl:DataLoaderView\u003e\n   ```\n\n1. Start your application! Note that by default using a `DataLoaderView` automatically triggers a initial load from the `IDataLoader`.\n\n## Features\n\n### Load async data\n\nThe load function of the `IDataLoader` must simply return a `Task` of the data type (here `MyData`).\n\n```csharp\nasync Task\u003cMyData\u003e LoadData(CancellationToken ct, IDataLoaderRequest request)\n{\n  // Put your async operation here.\n  return default(MyData);\n}\n```\n\nThis is where you would typically execute an API call or get data from any other async source.\n\nThis method receives a `CancellationToken` as the operation might be cancelled. Cancellation can happen for a few reasons.\n- When the `IDataLoader` is disposed.\n- If another request was made and the concurrent mode is `CancelPrevious`.\n\n### Get the DataLoader's state\n\nYou can check the state of a data loader using the `IDataLoader.State` property. You can observe its changes using the `IDataLoader.StateChanged` event.\n\nThe state of a DataLoader can be summarized as a combination of the following properties.\n\n|   State Property   | Description                   |\n|:------------------:|:------------------------------|\n| `IsLoading` | True during the async load execution. |\n|   `Data`    | Last data from a successful request.    |\n|   `Error`   | Error when the request failed.\u003cbr\u003e_Note: The error is cleared as soon as there is a successful request._ |\n\n#### Example\nImagine a scenario where you land on a page where the data has never been loaded before from an API.\n\n1. The system would first be on an initial state (not loading, no data, no error).\n2. The system would then start loading the data from the API.\n3. The system would then expose the data returned from the API.\n\nThis could be represented by the following state flow.\n```mermaid\nstateDiagram-v2\n  direction LR\n  s1: IsLoading(false)\u003cbr\u003eData(null)\u003cbr\u003eError(null)\n  s2: \u003cb\u003eIsLoading(true)\u003c/b\u003e\u003cbr\u003eData(null)\u003cbr\u003eError(null)\n  s3: IsLoading(false)\u003cbr\u003e\u003cb\u003eData(myData)\u003c/b\u003e\u003cbr\u003eError(null)\n\n  s1 --\u003e s2 : Start loading\n  s2 --\u003e s3 : Receive data\n```\nLet's go deeper and imagine that we lose network and do a pull to refresh.\n\n4. You lose connection and refresh the page.\n5. The system would then start loading the data from the API. \n6. The system notifies you that there was an error, but you still have your previous data.\n\nFrom the last sequence, we would continue with the following states.\n```mermaid\nstateDiagram-v2\n  direction LR\n  s3: IsLoading(false)\u003cbr\u003eData(myData)\u003cbr\u003eError(null)\n  s4: \u003cb\u003eIsLoading(true)\u003c/b\u003e\u003cbr\u003eData(myData)\u003cbr\u003eError(null)\n  s5: IsLoading(false)\u003cbr\u003eData(myData)\u003cbr\u003e\u003cb\u003eError(WebException)\u003c/b\u003e\n\n  s3 --\u003e s4 : Start refresh\n  s4 --\u003e s5 : Receive error\u003cbr\u003ebecause not network\n```\n\nOther useful state properties are available:\n\n| State Property | Description |\n|:---------:|:------------------------------------|\n| `IsInitial` | True by default. Becomes false when Data was set at least once.             |\n|  `IsEmpty`  | This is always false unless you define the rule using `IDataLoaderBuilder.IsEmptySelector`. |\n\n### Define `IsEmpty`\nApps often have a specific visual when content is empty.\nYou can configure your DataLoader to evaluate the `IDataLoaderState.IsEmpty` property based on a predicate of your choice.\nSimply use `IDataLoaderBuilder.WithEmptySelector`.\n```csharp\nMyAsyncList = DataLoaderBuilder\u003cList\u003cint\u003e\u003e\n  .Empty\n  .WithName(\"MyAsyncList\")\n  .WithLoadMethod(LoadMyAsyncList)\n  .WithEmptySelector(state =\u003e state.Data is null || state.Data.Count == 0)\n  .Build();\n```\n\n### DataLoaderView\n\nThe `DataLoaderView` is the UI equivalent of a `IDataLoader`.\nIt represents the different states of the `IDataLoader` using the following visual states.\nThis is the list of all supported visual states and their respective visual state group.\n\n| Visual State Group | Visual State |\n|:--------------:|:---------------------------|\n|   DataStates   | Initial                    |\n|   DataStates   | Data                       |\n|   DataStates   | Empty                      |\n|  ErrorStates   | NoError                    |\n|  ErrorStates   | Error                      |\n| LoadingStates  | NotLoading                 |\n| LoadingStates  | Loading                    |\n| CombinedStates | Initial_NoError_NotLoading |\n| CombinedStates | Initial_NoError_Loading    |\n| CombinedStates | Initial_Error_NotLoading   |\n| CombinedStates | Initial_Error_Loading      |\n| CombinedStates | Data_NoError_NotLoading    |\n| CombinedStates | Data_NoError_Loading       |\n| CombinedStates | Data_Error_NotLoading      |\n| CombinedStates | Data_Error_Loading         |\n| CombinedStates | Empty_NoError_NotLoading   |\n| CombinedStates | Empty_NoError_Loading      |\n| CombinedStates | Empty_Error_NotLoading     |\n| CombinedStates | Empty_Error_Loading        |\n\nHere is the table of all possible visual state combinations.\n\n| DataStates    | ErrorStates  | LoadingStates  | CombinedStates |\n|-|-|-|-|\n| Initial       |NoError        |NotLoading     | Initial_NoError_NotLoading |\n| Initial       |NoError        |Loading        | Initial_NoError_Loading    |\n| Initial       |Error          |NotLoading     | Initial_Error_NotLoading   |\n| Initial       |Error          |Loading        | Initial_Error_Loading      |\n| Data          |NoError        |NotLoading     | Data_NoError_NotLoading    |\n| Data          |NoError        |Loading        | Data_NoError_Loading       |\n| Data          |Error          |NotLoading     | Data_Error_NotLoading      |\n| Data          |Error          |Loading        | Data_Error_Loading         |\n| Empty         |NoError        |NotLoading     | Empty_NoError_NotLoading   |\n| Empty         |NoError        |Loading        | Empty_NoError_Loading      |\n| Empty         |Error          |NotLoading     | Empty_Error_NotLoading     |\n| Empty         |Error          |Loading        | Empty_Error_Loading        |\n\n\u003e 💡 You don't need to use all those states.\n\u003e It's possible to create a decent style without using the CombinedStates at all.\n\u003e The CombinedStates are available to offer support for advanced scenarios.\n\n`DataLoaderView` also has a few template properties to more easily define its `ControlTemplate`.\nYou can use any combination of the following in you styles.\nRemember that the _source of truth_ comes from the visual states.\n\n|         Template          | Suggested Usage |\n|:-------------------------:|:------------|\n|      `ContentTemplate`      | Template used when there is data to show.     |\n|       `EmptyTemplate`       | Template used when there is no data to show.        |\n|       `ErrorTemplate`       | Template used when an error occurred.       |\n| `ErrorNotificationTemplate` | Template used when an error occured but there is data to show.     |\n\n\u003e 💡 Again, you don't need to use all those properties.\n\u003e It's very possible that you don't use `ErrorNotificationTemplate` at all.\n\u003e Perhaps your style only uses `ContentTemplate`.\n\n### Protection against UI flickering\n\nTo avoid seeing flickery transitions between the different visual states, the `DataLoaderView` supports different throttling options.\n\n- `StateMinimumDuration`: This represents the minimum duration of a visual state.\nYou should keep this high enough to prevent visual states from changing too fast.\nThis is the property that prevents flickers.\n- `StateChangingThrottleDelay`: This represents the time before updating to a new state during which another update can happen.\nYou should keep this low enough to prevent slowing down your user experience.\nThis is the property that prevents seeing loading states that are not necessary.\n\n```xml\n\u003cdl:DataLoaderView Source=\"{Binding MyDataLoader}\"\n                   StateMinimumDuration=\"0:0:1.5\"\n                   StateChangingThrottleDelay=\"0:0:0.100\" /\u003e\n```\n\n### Triggers\nIt's possible to add `IDataLoaderTrigger` objects to a DataLoader using `IDataLoader.AddTrigger`.\nThis can be useful to automatically refresh the data.\nIt also clearly indicates what are the dependencies of DataLoader.\n\nThere are a few implementations provided.\n- `ManualDataLoaderTrigger`: Manually triggers a load request.\n- `ObservableDataLoaderTrigger`: Triggers a load request when the observable pushes a new value.\n\nThere are some more implementations in the `Chinook.DataLoader.DynamicMvvm` package.\n- `DynamicCommandDataLoaderTrigger`: Triggers a load request when the `IDynamicCommand.IsExecuting` property changes.\n- `DynamicPropertyDataLoaderTrigger`: Triggers a load request when the value of a `IDynamicProperty` changes.\n\nYou can also create your own triggers by inheriting from `DataLoaderTriggerBase`.\nSimply call `RaiseLoadRequested` when a load request should be requested.\nYou can also write a trigger from scratch by implementing `IDataLoaderTrigger`.\n\n\u003e 💡 `DataLoaderView` has a `AutoLoad` property which, as its name suggests, automatically requests an initial load from the `DataLoader` when it's displayed.\n\u003e This property defaults to `true` which means that you don't need to do anything to trigger the initial load when you use `DataLoaderView`.\n\n### Protection against concurrent loads\nNow that we know what can trigger loads, we can see how it could be possible to request loads _too fast_ and cause concurrent loads.\nThe DataLoader doesn't allow concurrent loads.\nThere are currently two behaviors to choose from when concurrent loads would happen.\n  - `CancelPrevious`: When there's a new request while a previous one is already loading, the previous request gets cancelled.\nThis is the default behavior.\n  - `DiscardNew`: When there's a new request while a previous one is already loading, the new request\ngets discarded (it doesn't even start).\n\nYou can set the desired behavior using `IDataLoaderBuilder.WithConcurrentMode`.\n\n### Requests and contexts\n\nTo provide metadata on the loading operation, two important pieces are used.\n\n- `IDataLoaderRequest`: Represents the request to load data.\n- `IDataLoaderContext`: Contains user-defined key-value-pairs. It's exposed from the `IDataLoaderRequest`.\n\nYou can get or set additional information from the `IDataLoader` using the `IDataLoaderRequest` parameter.\n\n```csharp\nTask\u003cMyData\u003e LoadData(CancellationToken ct, IDataLoaderRequest request)\n{\n  // This is the trigger that caused that loading operation.\n  var sourceTrigger = request.SourceTrigger;\n\n  // This is the context that was passed to the loading operation from the trigger.\n  var context = request.Context;\n\n  // You can read and write using the context.\n  context[\"myMessage\"] = \"Hello!\";\n\n  // Your async operation here.\n}\n```\n\nThe request is also available as part of the `IDataLoaderState` and `DataLoaderViewState`.\nThis means both request and context can be used in XAML data binding.\nThis can be useful when implementing custom styles of `DataLoaderView`.\n\nYou can bind to the context from a style's `ControlTemplate` as follows.\n```xml\n\u003cControlTemplate TargetType=\"dl:DataLoaderView\"\u003e\n  \u003cGrid\u003e\n    \u003c!-- (...) --\u003e\n    \u003cTextBlock Text=\"{Binding State.Request.Context[myMessage], RelativeSource={RelativeSource Mode=TemplatedParent}\"/\u003e\n  \u003c/Grid\u003e\n\u003c/ControlTemplate\u003e\n```\n\nTriggers can also provide values through the context.\nNote however that the values provided by a trigger are only accessible from the load function **when it originated from that trigger**.\n\n### Integration with Chinook.DynamicMvvm\n\nIf you use [`Chinook.DynamicMvvm`](https://github.com/nventive/Chinook.DynamicMvvm), know that there are built-in extensions methods that allow for fluent declaration of `IDataLoader`.\n\n1. Add the `Chinook.DataLoader.DynamicMvvm` package to your project.\n1. Register an `IDataLoaderBuilderFactory` in the `IServiceProvider` that your ViewModels use.\n   Here is a simple code sample that does that using `Microsoft.Extensions.DependencyInjection` and `Microsoft.Extensions.Hosting`.\n   ```csharp\n   var serviceProvider = new HostBuilder()\n     .ConfigureServices(serviceCollection =\u003e serviceCollection\n       .AddSingleton\u003cIDataLoaderBuilderFactory, IDataLoaderBuilderFactory\u003e()\n     )\n     .Build()\n     .Services;\n   ```\n1. Use `IViewModel.GetDataLoader` to declare your DataLoader.\n1. Optionally use `IViewModel.GetCommandFromDataLoaderRefresh` if you want a command that refreshes the DataLoader.\n(This can be useful for things like pull-to-refresh.)\n\n```csharp\npublic class MyViewModel : ViewModelBase\n{\n  public IDynamicCommand RefreshData =\u003e this.GetCommandFromDataLoaderRefresh(Data);\n\n  public IDataLoader Data =\u003e this.GetDataLoader(GetData, b =\u003e b\n    // You can optionally configure your data loader here\n  );\n\n  private async Task\u003cMyData\u003e GetData(CancellationToken ct)\n  {\n    // Get data from your API...\n  }\n}\n```\n\n\u003e 💡 You can install the Visual Studio Extension [Chinook Snippets](https://marketplace.visualstudio.com/items?itemName=nventivecorp.ChinookSnippets) and use code snippets to quickly generate DataLoader declarations when using [Chinook.DynamicMvvm](https://github.com/nventive/Chinook.DynamicMvvm).\n\u003e All snippets related to `IDataLoader` start with `ckdl` (for **C**hinoo**k** **D**ata**L**oader).\n\n### Automatically dispose previous data\nUsing `IDataLoaderBuilder.DisposePreviousData`, you can configure an `IDataLoader` that will automatically dispose its data when new data is loaded.\nThis works when `Data` directly implements `IDisposable` or when it's an `IEnumerable` containing items implementing `IDisposable`.\n\n```csharp\npublic IDataLoader\u003cDadJokesItemViewModel[]\u003e Jokes =\u003e this.GetDataLoader(LoadJokes, b =\u003e b\n    // Dispose the previous ItemViewModels when Jokes produces new values.\n    .DisposePreviousData()\n);\n```\n\n### Automatically re-evaluate `IsEmpty`\nSome types change on their own.\nA good example is `ObservableCollection`.\nDataLoader supports connecting to its `Data` in order to automatically update its state.\nUsing `IDataLoaderBuilder.UpdateOnCollectionChanged`, you can configure an `IDataLoader` that will automatically re-evaluate `IsEmpty` when the collection changes.\nThis works with any collection that implements `INotifyCollectionChanged`.\n```csharp\npublic IDataLoader\u003cObservableCollection\u003cToDoItem\u003e\u003e ToDoList =\u003e this.GetDataLoader(LoadToDoList, b =\u003e b\n    // Re-evaluate is Empty when the collection changes.\n    .UpdateOnCollectionChanged()\n);\n```\n\nFor types that don't implement `INotifyCollectionChanged`, you can use `IDataLoaderBuilder.SubscribeToData` to implement an equivalent behavior.\n\u003e ⚙ Note that `UpdateOnCollectionChanged` is actually implemented using `SubscribeToData`.\n\u003e The `SubscribeToData` building block allows this kind of behavior for any type that is _observable_, meaning an object that exposes an `event`, implements or exposes `IObservable\u003cT\u003e`, or has any other subscription mechanism.\n\n### Automatically dispose subscriptions\nYou might need intermediate objects that needs disposing when loading data.\nA good example is the subscription when creating an observable collection using DynamicData.\n```csharp\npublic IDataLoader\u003cReadOnlyObservableCollection\u003cint\u003e\u003e MyList =\u003e this.GetDataLoader(LoadMyList);\n\nprivate async Task\u003cReadOnlyObservableCollection\u003cint\u003e\u003e LoadMyList(CancellationToken ct, IDataLoaderRequest request)\n{\n  DynamicData.IObservableList\u003cint\u003e itemsSource = await GetItems(ct);\n  itemsSource\n    .Connect()\n    .Bind(out var list)\n    .Subscribe()\n    // Dispose this subscription when the next one is created.\n    .DisposeWithNextLoad(request);\n\n  return list;\n}\n```\nUsing the `IDisposable.DisposeWithNextLoad` extensions method, you can ensure that only 1 disposable stays active at a time.\nAlternatively, you can use the `IDataLoaderRequest.AddToSequenceDisposableGroup` extension method which does the same thing.\n\nNote that even when there are no subsequent loads, the disposable still gets disposed when the `IDataLoader` itself gets disposed.\n\n### Load on a background thread\nUsing `IDataLoaderBuilder.OnBackgroundThread`, you can configure an `IDataLoader` that will invoke its load function from `Task.Run`. This can be helpful to avoid loading data on the UI thread.\n```csharp\npublic IDataLoader\u003cstring\u003e Stuff =\u003e this.GetDataLoader(LoadStuff, b =\u003e b\n    // Invoke the load from a new task.\n    .OnBackgroundThread()\n);\n```\n\n### Define default behaviors\nUsing `DataLoaderBuilderFactory`, you can create a `IDataLoaderBuilderFactory` that provides default behaviors in all the `IDataLoaderBuilder` instances it creates.\nHere you can see how to create an `IDataLoaderBuilderFactory` using `Microsoft.Extensions.DependencyInjection`.\n```csharp\nprivate static IServiceCollection AddDataLoaderFactory(this IServiceCollection services)\n{\n  return services.AddSingleton\u003cIDataLoaderBuilderFactory\u003e(s =\u003e\n  {\n    return new DataLoaderBuilderFactory(b =\u003e b\n      .OnBackgroundThread()\n      .WithEmptySelector(GetIsEmpty)\n    );\n\n    bool GetIsEmpty(IDataLoaderState state)\n    {\n      return state.Data == null || (state.Data is IEnumerable enumerable \u0026\u0026    !enumerable.Cast\u003cobject\u003e().Any());\n    }\n  });\n}\n```\n\u003e 💡 When using `Chinook.DynamicMvvm`, the `IViewModel.GetDataLoader` extension methods automatically use `IDataLoaderBuilderFactory` registered in the `IServiceProvider` to generate the `IDataLoader` instances.\n\n### Support for custom behaviors\nThe architecture of `IDataLoader` and `IDataLoaderBuilder` was made in a way that more custom behaviors can be implemented.\nUsing `IDataLoaderBuilder.DelegatingStrategies`, you can add processing both before and after the actual load function in the same fashion as you would using a stack of `System.Net.Http.HttpMessageHandler`.\nCombining this with triggers, `IDataLoaderRequest`, and `IDataLoaderContext`, brings a lot of flexibility because the strategy has access to the request and context objects.\n\n\u003e ⚙ Even the recipes we've seen so far (like `SubscribeToData` or `DisposeWithNextLoad`) are built on top of that extensible foundation.\n\n### Configure a default refresh command for all DataLoaderViews\nIt's possible to configure a refresh command for all `DataLoaderView` instances using `DataLoaderView.DefaultRefreshCommandProvider`.\nWhen this property is set, it's used to set an initial value to `DataLoaderView.RefreshCommand`.\n\nThis can be useful to avoid repeating the same code in ViewModels.\nIt's also a great way to ensure all DataLoaderViews support manual refreshes, which enables things like putting a refresh button in you DataLoaderView's `ControlTemplate`.\n\nHere is an example to demonstrate how to use `DataLoaderView.DefaultRefreshCommandProvider`.\nIn this example, we leverage [`Chinook.DynamicMvvm`](https://github.com/nventive/Chinook.DynamicMvvm) to build the command.\n```csharp\nprivate void SetupDefaultRefreshCommand(IServiceProvider services)\n{\n  // DefaultRefreshCommandProvider is just a Func\u003cDataLoaderView, ICommand\u003e that's invoked when a DataLoaderView is created.\n  DataLoaderView.DefaultRefreshCommandProvider = GetDataLoaderViewRefreshCommand;\n\n  ICommand GetDataLoaderViewRefreshCommand(DataLoaderView dataLoaderView)\n  {\n    return services\n      .GetRequiredService\u003cIDynamicCommandBuilderFactory\u003e()\n      .CreateFromTask(\n        name: \"DataLoaderViewRefreshCommand\",\n        execute: async (ct) =\u003e\n        {\n          var context = new DataLoaderContext();\n          // You can add some content to the context.\n          context[\"IsForceRefreshing\"] = true;\n          await dataLoaderView.DataLoader.Load(ct, context);\n        },\n        viewModel: (IViewModel)App.Instance.Shell.DataContext)\n      .Build();\n  }\n}\n```\n\n## Breaking Changes\n\nPlease consult [BREAKING_CHANGES.md](BREAKING_CHANGES.md) for more information about breaking changes and version history.\n\n## License\n\nThis project is licensed under the Apache 2.0 license - see the\n[LICENSE](LICENSE) file for details.\n\n## Contributing\n\nPlease read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the process for\ncontributing to this project.\n\nBe mindful of our [Code of Conduct](CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnventive%2Fchinook.dataloader","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnventive%2Fchinook.dataloader","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnventive%2Fchinook.dataloader/lists"}