{"id":16687262,"url":"https://github.com/redth/maui.virtuallistview","last_synced_at":"2025-04-07T11:08:05.601Z","repository":{"id":41065448,"uuid":"331786456","full_name":"Redth/Maui.VirtualListView","owner":"Redth","description":"A slim ListView implementation for .NET MAUI that uses Platform virtualized lists / collections","archived":false,"fork":false,"pushed_at":"2024-06-11T21:24:39.000Z","size":68505,"stargazers_count":238,"open_issues_count":8,"forks_count":31,"subscribers_count":13,"default_branch":"main","last_synced_at":"2024-10-17T07:32:38.880Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Redth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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":"2021-01-21T23:52:05.000Z","updated_at":"2024-10-03T13:02:13.000Z","dependencies_parsed_at":"2023-12-05T01:44:39.377Z","dependency_job_id":"290da9e5-c256-4547-97c4-986b4489902a","html_url":"https://github.com/Redth/Maui.VirtualListView","commit_stats":null,"previous_names":["redth/xfslimlistview"],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Redth%2FMaui.VirtualListView","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Redth%2FMaui.VirtualListView/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Redth%2FMaui.VirtualListView/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Redth%2FMaui.VirtualListView/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Redth","download_url":"https://codeload.github.com/Redth/Maui.VirtualListView/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247640463,"owners_count":20971557,"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":[],"created_at":"2024-10-12T15:08:31.248Z","updated_at":"2025-04-07T11:08:05.582Z","avatar_url":"https://github.com/Redth.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# VirtualListView for .NET MAUI\nThis is an experiment in creating a virtualized ListView control for .NET MAUI to support simple, fast, multi-templated, uneven item sized lists by not adding too many bells and whistles and using an adapter pattern data source.\n\n[![Nuget: Redth.Maui.VirtualListView](https://img.shields.io/nuget/vpre/Redth.Maui.VirtualListView?logo=nuget\u0026label=Redth.Maui.VirtualListView\u0026color=004880\u0026link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRedth.Maui.VirtualListView%2F)](https://www.nuget.org/packages/Redth.Maui.VirtualListView)\n\n\n## Vroooom!\n\n![VirtualListView-Maui-MacCatalyst](https://user-images.githubusercontent.com/271950/129656785-ad302f84-4439-4f96-9405-29e62ed84861.gif)\n\nIn the sample, each item (and header/footer) is measured as it is recycled.  Performance is pretty great considering!  In the future there will be an option to tell the ListView if your template(s) are a consistent size so that the measure can be skipped for even better performance.\n\n## Native controls\nThe implementation uses fast native controls in its renderers and optimizes for the native platform's recycling strategies.  Items are cached through the platform's recycling mechanisms so that they can be reused efficiently.  This also means the MAUI representation of items are cached as well.  Each type of template (Item, Section Header, Section Footer) is cached individually so that they are reused efficiently.\n\nControls used on each platform:\n  - iOS: UICollectionView\n  - Android: RecyclerView\n  - WinAppSDK: ItemsRepeater + ScrollHost with IElementFactory\n\n## Setup\n\nTo add the Virtual List View control to your project, you need to add `.UseVirtualListView()` to your app builder:\n\n```csharp\n\t\tpublic static MauiApp Create()\n\t\t{\n\t\t\tvar builder = MauiApp.CreateBuilder();\n\t\t\tbuilder\n\t\t\t\t.UseMauiApp\u003cApp\u003e()\n\t\t\t\t.UseVirtualListView(); // \u003c--- THIS\n\t\t\treturn builder.Build();\n\t\t}\n```\n\n\n## Adapter / Data Source\n\nInstead of starting with a typical C# collection such as `ObservableCollection`, the VirtualListView takes the adapter approach that is common to iOS and Android has the concept of grouping built in (called Sections).\n\nThis pattern is optimal since it allows for easily creating adapters backed by direct access data stores such as databases.  Instead of trying to load data from the actual datastore, and trying to deal with cache invalidation for an in memory collection you can write your adapter directly against any type of storage.\n\nTo create an adapter for the VirtualListView, you need to implement the following interface:\n\n```csharp\npublic interface IVirtualListViewAdapter\n{\n\tint GetNumberOfSections();\n\n\tobject GetSection(int sectionIndex);\n\n\tint GetNumberOfItemsInSection(int sectionIndex);\n\n\tobject GetItem(int sectionIndex, int itemIndex);\n\n\tevent EventHandler OnDataInvalidated;\n\n\tvoid InvalidateData();\n}\n```\n\nThere are a few implementations included in the box to use:\n\n### `VirtualListViewAdapter`\n\nThis is a basic adapter backed by an `IList\u003cTItem\u003e`:\n\n```csharp\nvar adapter = new VirtualListViewAdapter\u003cstring\u003e(\n\tnew [] {\n\t\t\"Item 1\",\n\t\t\"Item 2\",\n\t\t\"Item 3\",\n\t\t//...\n\t});\n```\n\n### `ObservableCollectionAdapter`\n\nMany developers are accustomed to using `ObservableCollection`.  While I recommend against using this if possible, there is a built in adapter that takes in an `ObservableCollection\u003cTItem\u003e` instance to help map it to the adapter pattern:\n\n```csharp\nvar items = new ObservableCollection\u003cstring\u003e();\nitems.Add(\"Item 1\");\nitems.Add(\"Item 2\");\n\nvar adapter = new ObservableCollectionAdapter\u003cstring\u003e(items);\n\nitems.Add(\"Item 3\");\n```\n\nWhen using this adapter, the adapter will automatically invalidate itself (by calling `this.InvalidateData()` when the collection changes via the `CollectionView.CollectionChanged` event).\n\n\n### Custom Adapter\n\nFor many scenarios, it is ideal to create your own adapter implementation.  You can implement the `IVirtualListViewAdapter` directly, or subclass `VirtualListViewAdapterBase\u003cTSection, TItem\u003e`.\n\nHere's an example of a custom adapter for a flat list (no sections/grouping).  Notice that we cache commonly used data such as `ItemsForSection` and we will reset the cache anytime the data is invalidated:\n\n```csharp\npublic class SQLiteAdapter : VirtualListViewAdapterBase\u003cobject, ItemInfo\u003e\n{\n\tpublic SQLiteAdapter() : base()\n\t{\n\t\tDb = new Database(...);\n\t}\n\n\tpublic Database Db { get; }\n\n\tint? cachedItemCount = null;\n\n\t// No sections/grouping, so disregard the sectionIndex\n\tpublic override int GetNumberOfItemsInSection(int sectionIndex)\n\t\t=\u003e cachedItemCount ??= Db.ExecuteScalar\u003cint\u003e(\"SELECT COUNT(Id) FROM Items\");\n\n\tpublic override string GetItem(int sectionIndex, int itemIndex)\n\t\t=\u003e Db.FindWithQuery\u003cItemInfo\u003e(\"SELECT * FROM Items ORDER BY Id LIMIT 1 OFFSET ?\", itemIndex);\n\n\tpublic override void InvalidateData()\n\t{\n\t\t// Clear our item count cache\n\t\t// Also do this any time we may insert or delete data \n\t\tcachedItemCount = null;\n\t\tbase.InvalidateData();\n\t}\n}\n```\n\nHere's an example of a more sophisticated adapter with grouping/sections. Again, notice we cache Section count and Item count per section:\n\n```csharp\npublic class SQLiteSectionedAdapter : VirtualListViewAdapterBase\u003cGroupInfo, ItemInfo\u003e\n{\n\tpublic SQLiteSectionedAdapter() : base()\n\t{\n\t\tDb = new Database(...);\n\t}\n\n\tpublic Database Db { get; }\n\n\tDictionary\u003cint, GroupInfo\u003e cachedSectionSummaries = new ();\n\n\tint? cachedNumberOfSections = null;\n\n\tpublic int GetNumberOfSections()\n\t\t=\u003e cachedNumberOfSections ??= Db.ExecuteScalar\u003cint\u003e(\"SELECT DISTINCT COUNT(GroupId) FROM Items\");\n\n\t// No sections/grouping, so disregard the sectionIndex\n\tpublic override int GetNumberOfItemsInSection(int sectionIndex)\n\t\t=\u003e cachedItemCount ??= Db.ExecuteScalar\u003cint\u003e(\"SELECT COUNT(Id) FROM Items\");\n\n\tpublic GroupInfo GetSection(int sectionIndex)\n\t{\n\t\tif (cachedSectionSummaries.ContainsKey(sectionIndex))\n\t\t\treturn cachedSectionSummaries[sectionIndex];\n\n\t\tvar sql = @\"\n\t\t\t\tSELECT DISTINCT g.GroupId, g.GroupName, Count(i.Id) as ItemCount\n\t\t\t\tFROM Items g\n\t\t\t\t\tINNER JOIN Items i ON i.GroupId = g.GroupId\n\t\t\t\tGROUP BY g.GroupId\n\t\t\t\tORDER BY g.GroupName\n\t\t\t\tLIMIT 1 OFFSET ?\n\t\t\";\n\n\t\tvar groupInfo = Db.FindWithQuery\u003cGroupInfo\u003e(sql, sectionIndex);\n\n\t\tif (groupInfo != null)\n\t\t\tcachedSectionSummaries.Add(sectionIndex, groupInfo);\n\n\t\treturn groupInfo;\n\t}\n\n\tpublic override string GetItem(int sectionIndex, int itemIndex)\n\t\t=\u003e Db.FindWithQuery\u003cItemInfo\u003e(\"SELECT * FROM Items WHERE GroupId=? ORDER BY Id LIMIT 1 OFFSET ?\", sectionIndex, itemIndex);\n\n\tpublic override void InvalidateData()\n\t{\n\t\t// Clear our caches\n\t\t// Also do this any time we may insert or delete data \n\t\tcachedItemCount = null;\n\t\tcachedNumberOfSections.Clear();\n\n\t\tbase.InvalidateData();\n\t}\n}\n```\n\n\n\n## Templates\n\nDataTemplates are available for Items along with an Item Template Selector (which is a custom class based on the adapter pattern to select templates).\n\nDifferent templates can be specified for:\n - Global Header\n - Global Footer\n - Section Header\n - Section Footer\n - Item\n\nIn addition, it's possible to use Template Selectors which allow you to use different DataTemplates depending on the section / item index being displayed.\n\nTemplate selectors are available for:\n - Section Headers and Footers\n - Items\n\n\nFor Item template selectors, sublcass the `AdapterItemDataTemplateSelector`:\n\n```csharp\npublic class MyItemTemplateSelector\n{\n\tPersonTemplate personTemplate = new PersonTemplate();\n\tGenericTemplate genericTemplate = new GenericTemplate();\n\n\tpublic override DataTemplate SelectItemTemplate(IVirtualListViewAdapter adapter, int sectionIndex, int itemIndex)\n\t{\n\t\tvar item = adapter.GetItem(sectionIndex, itemIndex);\n\n\t\tif (item is Person)\n\t\t\treturn personTemplate;\n\t\t\n\t\treturn genericTemplate;\n\t}\n}\n```\n\nFor section template selectors, subclass `AdapterSectionDataTemplateSelector`.\n\n## Virtual ViewCells\n\nAll templates can contain a single `IView`, or alternatively you can use `VirtualViewCell` to wrap your view.\n\nThe `VirtualViewCell`'s `ResourceDictionary` will contain a set of values which are are useful for adapting your views for things like separators and selection state:\n\n  - int SectionIndex\n  - int ItemIndex\n  - bool IsGlobalHeader\n  - bool IsGlobalFooter\n  - bool IsSectionHeader\n  - bool IsSectionFooter\n  - bool IsItem\n  - bool IsLastItemInSection\n  - bool IsNotLastItemInSection\n  - bool IsFirstItemInSection\n  - bool IsNotFirstItemInSection\n  - bool IsSelected\n\n\u003e NOTE: These are also available as properties on `VirtualViewCell` itself, since it implements `IPositionInfo`\n\nYou can access these properties from your templates.  Here's an example of displaying an item separator using these properties, as well as changing the background color based on the selection state and a converter:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cxct:VirtualViewCell\n\txmlns:xct=\"clr-namespace:Microsoft.Maui.Controls;assembly=VirtualListView\"\n\txmlns=\"http://xamarin.com/schemas/2014/forms\" \n\txmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n\tx:Class=\"VirtualListViewSample.GenericViewCell\"\u003e\n  \u003cxct:VirtualViewCell\u003e\n\t\t\u003cVerticalStackLayout\n\t\t\tSpacing=\"0\"\n\t\t\tBackgroundColor=\"{Binding Source={x:Reference self}, Path=IsSelected, Converter={StaticResource selectedColorConverter}}\"\u003e\n\n\t\t\t\u003cBoxView\n\t\t\t\tHorizontalOptions=\"FillAndExpand\"\n\t\t\t\tHeightRequest=\"1\"\n\t\t\t\tBackgroundColor=\"#f8f8f8\"\n\t\t\t\tIsVisible=\"{DynamicResource IsNotFirstItemInSection}\" \u003c!-- Use the automatic property --\u003e\n \t\t\t\t/\u003e\n\n\t\t\t\u003cBorder Background=\"#f0f0f0\" StrokeShape=\"{RoundedRectangle CornerRadius=14}\" Margin=\"10,5,10,5\" Padding=\"10\"\u003e\n\t\t\t\t\u003cLabel Text=\"{Binding TrackName}\" /\u003e\n\t\t\t\u003c/Border\u003e\n\n\t\t\u003c/VerticalStackLayout\u003e\n\t\u003c/xct:VirtualViewCell\u003e\n\u003c/xct:VirtualViewCell\u003e\n```\n\nNotice the `IsVisible=\"{DynamicResource IsNotFirstItemInSection}\"` references a resource which has been automatically populated by the `VirtualViewCell`.\n\n## Selection\n\nThere are 3 selection modes: `None`, `Single`, and `Multiple`.  Only `Item` types are selectable.\n\nThere are `SelectedItem` and `SelectedItems` bindable properties.\nThere's an `OnSelectedItemsChanged` event fired whenever these change.\n\n## Refreshing\n\nPull to refresh is enabled for iOS/MacCatalyst and Android.  WindowsAppSDK does not have the equivalent feature so there is no support for it.\nYou can use the `RefreshCommand` or subscribe to the `OnRefresh` event to perform your logic while the refresh indicator displays.\nYou must set `IsRefreshEnabled` to true to enable the gesture.  \nYou can also set the `RefreshAccentColor` to change the color of the refresh indicator.\n\n## Empty View\n\nIf your adapter has \u003c= 1 section and no items, an empty view can be displayed automatically:\n\n```xaml\n\u003cvlv:VirtualListView.EmptyView\u003e\n  \u003cGrid\u003e\n    \u003cLabel HorizontalOptions=\"Center\" VerticalOptions=\"Center\" Text=\"EMPTY\" /\u003e\n  \u003c/Grid\u003e\n\u003c/vlv:VirtualListView.EmptyView\u003e\n```\n\n\n## Scrolled\n\nScrolled notifications can be observed with `ScrolledCommand` which will pass a `ScrolledEventArgs` parameter, or the `OnScrolled` event with a parameter of the same type.  The event args contain the X/Y position scrolled.\n\n\n## Future\n\nLooking ahead, there are a few goals:\n\n1. Even Rows - by default every cell is assumed uneven and measured every time the context changes or the cell is recycled.  Adding an option to assume each template type is the same size will make performance even better, but will be an explicit opt-in\n2. Supporting \"size of content\" constraints\n\nSome current non-goals but considerations for even later:\n- Grid / Column support\n- Sticky section headers\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredth%2Fmaui.virtuallistview","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fredth%2Fmaui.virtuallistview","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredth%2Fmaui.virtuallistview/lists"}