From a29ca2904c78857ddf08a06d22b1d24c88834b54 Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Wed, 24 Jan 2024 14:11:23 -0500 Subject: [PATCH 1/9] Update README.md with nuget link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index edf5c6c..82bec0d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # VirtualListView for .NET MAUI This 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. -![Nuget: Redth.Maui.VirtualListView](https://img.shields.io/nuget/vpre/Redth.Maui.VirtualListView?logo=nuget&label=Redth.Maui.VirtualListView&color=004880&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRedth.Maui.VirtualListView%2F) +[![Nuget: Redth.Maui.VirtualListView](https://img.shields.io/nuget/vpre/Redth.Maui.VirtualListView?logo=nuget&label=Redth.Maui.VirtualListView&color=004880&link=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FRedth.Maui.VirtualListView%2F)](https://www.nuget.org/packages/Redth.Maui.VirtualListView) ## Vroooom! From ca708e9482ab15757975f53f477bae547a00886d Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 18 Apr 2024 19:42:46 -0400 Subject: [PATCH 2/9] Better null handling In CvCell in my app I was getting some weird null exceptions that while they do not make a lot of sense to get to the GetCell call, I can do a bit better job of mitigating those exceptions to originate from this library. --- .../Apple/CvCell.ios.maccatalyst.cs | 2 + .../Apple/CvDataSource.ios.maccatalyst.cs | 77 ++++--- .../Apple/CvDelegate.ios.maccatalyst.cs | 23 +- VirtualListView/Controls/VirtualListView.cs | 214 +++++++++--------- 4 files changed, 171 insertions(+), 145 deletions(-) diff --git a/VirtualListView/Apple/CvCell.ios.maccatalyst.cs b/VirtualListView/Apple/CvCell.ios.maccatalyst.cs index 7e0eff3..517b642 100644 --- a/VirtualListView/Apple/CvCell.ios.maccatalyst.cs +++ b/VirtualListView/Apple/CvCell.ios.maccatalyst.cs @@ -9,6 +9,8 @@ namespace Microsoft.Maui; internal class CvCell : UICollectionViewCell { + internal const string ReuseIdUnknown = "UNKNOWN"; + public VirtualListViewHandler Handler { get; set; } public WeakReference IndexPath { get; set; } diff --git a/VirtualListView/Apple/CvDataSource.ios.maccatalyst.cs b/VirtualListView/Apple/CvDataSource.ios.maccatalyst.cs index be6bf5a..2d2dd99 100644 --- a/VirtualListView/Apple/CvDataSource.ios.maccatalyst.cs +++ b/VirtualListView/Apple/CvDataSource.ios.maccatalyst.cs @@ -1,4 +1,5 @@ -using Foundation; +#nullable enable +using Foundation; using UIKit; namespace Microsoft.Maui; @@ -20,54 +21,74 @@ public CvDataSource(VirtualListViewHandler handler) public override nint NumberOfSections(UICollectionView collectionView) => 1; - + public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath) { var info = Handler?.PositionalViewSelector?.GetInfo(indexPath.Item.ToInt32()); - var data = Handler?.PositionalViewSelector?.Adapter?.DataFor(info.Kind, info.SectionIndex, info.ItemIndex); - - var reuseId = Handler?.PositionalViewSelector?.ViewSelector?.GetReuseId(info, data); + object? data = null; - var nativeReuseId = info.Kind switch + var nativeReuseId = CvCell.ReuseIdUnknown; + + if (info is not null) { - PositionKind.Item => itemIdManager.GetReuseId(collectionView, reuseId), - PositionKind.SectionHeader => sectionHeaderIdManager.GetReuseId(collectionView, reuseId), - PositionKind.SectionFooter => sectionFooterIdManager.GetReuseId(collectionView, reuseId), - PositionKind.Header => globalIdManager.GetReuseId(collectionView, reuseId), - PositionKind.Footer => globalIdManager.GetReuseId(collectionView, reuseId), - _ => "UNKNOWN", - }; - - var cell = (collectionView.DequeueReusableCell(nativeReuseId, indexPath) as CvCell)!; + data = Handler?.PositionalViewSelector?.Adapter?.DataFor(info.Kind, info.SectionIndex, info.ItemIndex); + + if (data is not null) + { + var reuseId = Handler?.PositionalViewSelector?.ViewSelector?.GetReuseId(info, data); + + nativeReuseId = info.Kind switch + { + PositionKind.Item => itemIdManager.GetReuseId(collectionView, reuseId), + PositionKind.SectionHeader => sectionHeaderIdManager.GetReuseId(collectionView, reuseId), + PositionKind.SectionFooter => sectionFooterIdManager.GetReuseId(collectionView, reuseId), + PositionKind.Header => globalIdManager.GetReuseId(collectionView, reuseId), + PositionKind.Footer => globalIdManager.GetReuseId(collectionView, reuseId), + _ => CvCell.ReuseIdUnknown, + }; + } + } + + var nativeCell = collectionView.DequeueReusableCell(nativeReuseId, indexPath); + if (nativeCell is not CvCell cell) + return (UICollectionViewCell)nativeCell; + cell.SetTapHandlerCallback(TapCellHandler); cell.Handler = Handler; cell.IndexPath = new WeakReference(indexPath); cell.ReuseCallback = new WeakReference>((rv) => { - if (cell?.VirtualView?.TryGetTarget(out var cellVirtualView) ?? false) - Handler.VirtualView.ViewSelector.ViewDetached(info, cellVirtualView); + if (info is not null && (cell.VirtualView?.TryGetTarget(out var cellView) ?? false)) + Handler?.VirtualView?.ViewSelector?.ViewDetached(info, cellView); }); - if (info.SectionIndex < 0 || info.ItemIndex < 0) - info.IsSelected = false; - else - info.IsSelected = Handler?.IsItemSelected(info.SectionIndex, info.ItemIndex) ?? false; + if (info is not null) + { + if (info.SectionIndex < 0 || info.ItemIndex < 0) + info.IsSelected = false; + else + info.IsSelected = Handler?.IsItemSelected(info.SectionIndex, info.ItemIndex) ?? false; + } - if (cell.NeedsView) + if (cell.NeedsView && info is not null && data is not null) { var view = Handler?.PositionalViewSelector?.ViewSelector?.CreateView(info, data); - cell.SetupView(view); + if (view is not null) + cell.SetupView(view); } - cell.UpdatePosition(info); - - if (cell.VirtualView.TryGetTarget(out var cellVirtualView)) + if (info is not null) { - Handler?.PositionalViewSelector?.ViewSelector?.RecycleView(info, data, cellVirtualView); + cell.UpdatePosition(info); + + if (data is not null && (cell.VirtualView?.TryGetTarget(out var cellVirtualView) ?? false)) + { + Handler?.PositionalViewSelector?.ViewSelector?.RecycleView(info, data, cellVirtualView); - Handler.VirtualView.ViewSelector.ViewAttached(info, cellVirtualView); + Handler?.VirtualView?.ViewSelector?.ViewAttached(info, cellVirtualView); + } } return cell; diff --git a/VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs b/VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs index c67e3f9..3870a0a 100644 --- a/VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs +++ b/VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs @@ -11,6 +11,7 @@ public CvDelegate(VirtualListViewHandler handler, UICollectionView collectionVie { Handler = handler; NativeCollectionView = new WeakReference(collectionView); + collectionView.RegisterClassForCell(typeof(CvCell), CvCell.ReuseIdUnknown); } internal readonly WeakReference NativeCollectionView; @@ -27,20 +28,22 @@ public override void ItemDeselected(UICollectionView collectionView, NSIndexPath void HandleSelection(UICollectionView collectionView, NSIndexPath indexPath, bool selected) { //UIView.AnimationsEnabled = false; - var selectedCell = collectionView.CellForItem(indexPath) as CvCell; - - if ((selectedCell?.PositionInfo?.Kind ?? PositionKind.Header) == PositionKind.Item) + if (collectionView.CellForItem(indexPath) is CvCell selectedCell + && (selectedCell.PositionInfo?.Kind ?? PositionKind.Header) == PositionKind.Item) { selectedCell.UpdateSelected(selected); - var itemPos = new ItemPosition( - selectedCell.PositionInfo.SectionIndex, - selectedCell.PositionInfo.ItemIndex); + if (selectedCell.PositionInfo is not null) + { + var itemPos = new ItemPosition( + selectedCell.PositionInfo.SectionIndex, + selectedCell.PositionInfo.ItemIndex); - if (selected) - Handler?.VirtualView?.SelectItem(itemPos); - else - Handler?.VirtualView?.DeselectItem(itemPos); + if (selected) + Handler?.VirtualView?.SelectItem(itemPos); + else + Handler?.VirtualView?.DeselectItem(itemPos); + } } } diff --git a/VirtualListView/Controls/VirtualListView.cs b/VirtualListView/Controls/VirtualListView.cs index fda0aa0..4eb3371 100644 --- a/VirtualListView/Controls/VirtualListView.cs +++ b/VirtualListView/Controls/VirtualListView.cs @@ -1,6 +1,6 @@ using System.Windows.Input; -using Microsoft.Maui.Adapters; - +using Microsoft.Maui.Adapters; + namespace Microsoft.Maui.Controls; public partial class VirtualListView : View, IVirtualListView, IVirtualListViewSelector @@ -30,13 +30,13 @@ public IView GlobalHeader public static readonly BindableProperty GlobalHeaderProperty = BindableProperty.Create(nameof(GlobalHeader), typeof(IView), typeof(VirtualListView), default); - public bool IsHeaderVisible - { - get => (bool)GetValue(IsHeaderVisibleProperty); - set => SetValue(IsHeaderVisibleProperty, value); - } - - public static readonly BindableProperty IsHeaderVisibleProperty = + public bool IsHeaderVisible + { + get => (bool)GetValue(IsHeaderVisibleProperty); + set => SetValue(IsHeaderVisibleProperty, value); + } + + public static readonly BindableProperty IsHeaderVisibleProperty = BindableProperty.Create(nameof(IsHeaderVisible), typeof(bool), typeof(VirtualListView), true); public IView GlobalFooter @@ -49,14 +49,14 @@ public IView GlobalFooter BindableProperty.Create(nameof(GlobalFooter), typeof(IView), typeof(VirtualListView), default); - public bool IsFooterVisible - { - get => (bool)GetValue(IsFooterVisibleProperty); - set => SetValue(IsFooterVisibleProperty, value); - } - - public static readonly BindableProperty IsFooterVisibleProperty = - BindableProperty.Create(nameof(IsFooterVisible), typeof(bool), typeof(VirtualListView), true); + public bool IsFooterVisible + { + get => (bool)GetValue(IsFooterVisibleProperty); + set => SetValue(IsFooterVisibleProperty, value); + } + + public static readonly BindableProperty IsFooterVisibleProperty = + BindableProperty.Create(nameof(IsFooterVisible), typeof(bool), typeof(VirtualListView), true); public DataTemplate ItemTemplate @@ -148,26 +148,26 @@ public ICommand RefreshCommand } public static readonly BindableProperty RefreshCommandProperty = - BindableProperty.Create(nameof(RefreshCommand), typeof(ICommand), typeof(VirtualListView), default); - - public Color RefreshAccentColor - { - get => (Color)GetValue(RefreshAccentColorProperty); - set => SetValue(RefreshAccentColorProperty, value); - } - - public static readonly BindableProperty RefreshAccentColorProperty = - BindableProperty.Create(nameof(RefreshAccentColor), typeof(Color), typeof(VirtualListView), null); - - public bool IsRefreshEnabled - { - get => (bool)GetValue(IsRefreshEnabledProperty); - set => SetValue(IsRefreshEnabledProperty, value); - } - - public static readonly BindableProperty IsRefreshEnabledProperty = - BindableProperty.Create(nameof(IsRefreshEnabled), typeof(bool), typeof(VirtualListView), false); - + BindableProperty.Create(nameof(RefreshCommand), typeof(ICommand), typeof(VirtualListView), default); + + public Color RefreshAccentColor + { + get => (Color)GetValue(RefreshAccentColorProperty); + set => SetValue(RefreshAccentColorProperty, value); + } + + public static readonly BindableProperty RefreshAccentColorProperty = + BindableProperty.Create(nameof(RefreshAccentColor), typeof(Color), typeof(VirtualListView), null); + + public bool IsRefreshEnabled + { + get => (bool)GetValue(IsRefreshEnabledProperty); + set => SetValue(IsRefreshEnabledProperty, value); + } + + public static readonly BindableProperty IsRefreshEnabledProperty = + BindableProperty.Create(nameof(IsRefreshEnabled), typeof(bool), typeof(VirtualListView), false); + public ListOrientation Orientation { get => (ListOrientation)GetValue(OrientationProperty); @@ -178,25 +178,25 @@ public ListOrientation Orientation BindableProperty.Create(nameof(Orientation), typeof(ListOrientation), typeof(VirtualListView), ListOrientation.Vertical); - public View EmptyView - { - get => (View)GetValue(EmptyViewProperty); - set => SetValue(EmptyViewProperty, value); - } - - public static readonly BindableProperty EmptyViewProperty = - BindableProperty.Create(nameof(EmptyView), typeof(View), typeof(VirtualListView), null, - propertyChanged: (bobj, oldValue, newValue) => - { - if (bobj is VirtualListView virtualListView) - { - if (oldValue is IView oldView) - virtualListView.RemoveLogicalChild(oldView); - - if (newValue is IView newView) - virtualListView.AddLogicalChild(newView); - } - }); + public View EmptyView + { + get => (View)GetValue(EmptyViewProperty); + set => SetValue(EmptyViewProperty, value); + } + + public static readonly BindableProperty EmptyViewProperty = + BindableProperty.Create(nameof(EmptyView), typeof(View), typeof(VirtualListView), null, + propertyChanged: (bobj, oldValue, newValue) => + { + if (bobj is VirtualListView virtualListView) + { + if (oldValue is IView oldView) + virtualListView.RemoveLogicalChild(oldView); + + if (newValue is IView newView) + virtualListView.AddLogicalChild(newView); + } + }); IView IVirtualListView.EmptyView => EmptyView; @@ -225,46 +225,46 @@ public ICommand ScrolledCommand { get => (ICommand)GetValue(ScrolledCommandProperty); set => SetValue(ScrolledCommandProperty, value); - } - + } + public static readonly BindableProperty SelectedItemsProperty = - BindableProperty.Create(nameof(SelectedItems), typeof(IList), typeof(VirtualListView), Array.Empty(), - propertyChanged: (bindableObj, oldValue, newValue) => - { - if (bindableObj is VirtualListView vlv - && oldValue is IList oldSelection - && newValue is IList newSelection) - { - vlv.RaiseSelectedItemsChanged(oldSelection.ToArray(), newSelection.ToArray()); - } - }); - public IList SelectedItems - { - get => (IList)GetValue(SelectedItemsProperty); - set => SetValue(SelectedItemsProperty, value ?? Array.Empty()); - } - + BindableProperty.Create(nameof(SelectedItems), typeof(IList), typeof(VirtualListView), Array.Empty(), + propertyChanged: (bindableObj, oldValue, newValue) => + { + if (bindableObj is VirtualListView vlv + && oldValue is IList oldSelection + && newValue is IList newSelection) + { + vlv.RaiseSelectedItemsChanged(oldSelection.ToArray(), newSelection.ToArray()); + } + }); + public IList SelectedItems + { + get => (IList)GetValue(SelectedItemsProperty); + set => SetValue(SelectedItemsProperty, value ?? Array.Empty()); + } + public static readonly BindableProperty SelectedItemProperty = - BindableProperty.Create(nameof(SelectedItem), typeof(ItemPosition?), typeof(VirtualListView), default, - propertyChanged: (bindableObj, oldValue, newValue) => - { - if (bindableObj is VirtualListView vlv) - { - if (newValue is null || newValue is not ItemPosition) - vlv.SelectedItems = null; - else if (newValue is ItemPosition p) - vlv.SelectedItems = new[] { p }; - } + BindableProperty.Create(nameof(SelectedItem), typeof(ItemPosition?), typeof(VirtualListView), default, + propertyChanged: (bindableObj, oldValue, newValue) => + { + if (bindableObj is VirtualListView vlv) + { + if (newValue is null || newValue is not ItemPosition) + vlv.SelectedItems = null; + else if (newValue is ItemPosition p) + vlv.SelectedItems = new[] { p }; + } }); - public ItemPosition? SelectedItem - { - get => (ItemPosition?)GetValue(SelectedItemProperty); - set => SetValue(SelectedItemProperty, value); - } - + public ItemPosition? SelectedItem + { + get => (ItemPosition?)GetValue(SelectedItemProperty); + set => SetValue(SelectedItemProperty, value); + } + - public void DeselectItem(ItemPosition itemPosition) + public void DeselectItem(ItemPosition itemPosition) { if (SelectionMode == Maui.SelectionMode.Single) { @@ -280,17 +280,17 @@ public void DeselectItem(ItemPosition itemPosition) { current.Remove(itemPosition); SelectedItems = current.ToArray(); - } - } + } + } } - public void SelectItem(ItemPosition itemPosition) + public void SelectItem(ItemPosition itemPosition) { if (SelectionMode == Maui.SelectionMode.Single) { - if (!SelectedItem.HasValue || !SelectedItem.Value.Equals(itemPosition)) - { - SelectedItem = itemPosition; + if (!SelectedItem.HasValue || !SelectedItem.Value.Equals(itemPosition)) + { + SelectedItem = itemPosition; } } else if (SelectionMode == Maui.SelectionMode.Multiple) @@ -300,15 +300,15 @@ public void SelectItem(ItemPosition itemPosition) { SelectedItems = current.Append(itemPosition).ToArray(); } - } + } } - public void ClearSelectedItems() + public void ClearSelectedItems() { if (SelectionMode == Maui.SelectionMode.Multiple) - SelectedItems = null; - else - SelectedItem = null; + SelectedItems = null; + else + SelectedItem = null; } public void ScrollToItem(ItemPosition itemPosition, bool animated) @@ -321,9 +321,9 @@ public bool SectionHasFooter(int sectionIndex) => SectionFooterTemplateSelector != null || SectionFooterTemplate != null; public IView CreateView(PositionInfo position, object data) - => position.Kind switch + => position.Kind switch { - PositionKind.Item => + PositionKind.Item => ItemTemplateSelector?.SelectTemplate(data, position.SectionIndex, position.ItemIndex)?.CreateContent() as View ?? ItemTemplate?.CreateContent() as View, PositionKind.SectionHeader => @@ -378,9 +378,9 @@ public void ViewDetached(PositionInfo position, IView view) => this.RemoveLogicalChild(view); public void ViewAttached(PositionInfo position, IView view) - => this.AddLogicalChild(view); - - void RaiseSelectedItemsChanged(ItemPosition[] previousSelection, ItemPosition[] newSelection) + => this.AddLogicalChild(view); + + void RaiseSelectedItemsChanged(ItemPosition[] previousSelection, ItemPosition[] newSelection) => this.OnSelectedItemsChanged?.Invoke(this, new SelectedItemsChangedEventArgs(previousSelection, newSelection)); } From 75c1bc9047f345a3c6701c1c118c211150724a82 Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 18 Apr 2024 19:49:36 -0400 Subject: [PATCH 3/9] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a73a38f..68f4899 100644 --- a/.gitignore +++ b/.gitignore @@ -349,3 +349,5 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ .DS_Store + +.idea/ From 288eeaf5a6a91a31fabcb0913476d1533eda515a Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 18 Apr 2024 19:50:37 -0400 Subject: [PATCH 4/9] Fix scroll handler This weakref was not sticking around, so reverting back to a normal delegate. Fixes #33 --- VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs | 5 ++--- .../Apple/VirtualListViewHandler.ios.maccatalyst.cs | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs b/VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs index 3870a0a..b1deabe 100644 --- a/VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs +++ b/VirtualListView/Apple/CvDelegate.ios.maccatalyst.cs @@ -17,7 +17,7 @@ public CvDelegate(VirtualListViewHandler handler, UICollectionView collectionVie internal readonly WeakReference NativeCollectionView; internal readonly VirtualListViewHandler Handler; - public WeakReference> ScrollHandler { get; set; } + public Action ScrollHandler { get; set; } public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath) => HandleSelection(collectionView, indexPath, true); @@ -49,8 +49,7 @@ void HandleSelection(UICollectionView collectionView, NSIndexPath indexPath, boo public override void Scrolled(UIScrollView scrollView) { - if (ScrollHandler?.TryGetTarget(out var handler) ?? false) - handler?.Invoke(scrollView.ContentOffset.X, scrollView.ContentOffset.Y); + ScrollHandler?.Invoke(scrollView.ContentOffset.X, scrollView.ContentOffset.Y); } public override bool ShouldSelectItem(UICollectionView collectionView, NSIndexPath indexPath) diff --git a/VirtualListView/Apple/VirtualListViewHandler.ios.maccatalyst.cs b/VirtualListView/Apple/VirtualListViewHandler.ios.maccatalyst.cs index 90a5579..6879f42 100644 --- a/VirtualListView/Apple/VirtualListViewHandler.ios.maccatalyst.cs +++ b/VirtualListView/Apple/VirtualListViewHandler.ios.maccatalyst.cs @@ -62,8 +62,7 @@ protected override void ConnectHandler(UICollectionView nativeView) dataSource = new CvDataSource(this); cvdelegate = new CvDelegate(this, nativeView); - cvdelegate.ScrollHandler = new WeakReference>((x, y) => - VirtualView?.Scrolled(x, y)); + cvdelegate.ScrollHandler = (x, y) => VirtualView?.Scrolled(x, y); nativeView.DataSource = dataSource; nativeView.Delegate = cvdelegate; From fa5dfd0b94a98f8228da6abb69567087e59b53d6 Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 18 Apr 2024 20:27:20 -0400 Subject: [PATCH 5/9] Hide Global/Header Footer when adapter is empty As described in #31 when the adapter is empty, the global header/footer (which are displayed in cells) overlap the empty view. While the desired behaviour in some cases might be to make the emptyview fill the remaining space that the global header/footer do not occupy, the problem is the header/footer are implemented as cells in the list (so they scroll with the content of the list - if you didn't want them to scroll, you'd put them outside of the list anyway), so it would be challenging to make the empty view a cell which fills the remaining space. This change instead hides the global header/footer if the adapter is empty, so that the empty view fills the entire space. If you really want the same global header/footer to appear when the adapter is empty, you can simply add them also to your empty view template (eg: use a grid with `RowDefinitions="Auto,*Auto"` where your header and footer are rows 0 and 2 and the middle view fills the remaining space. --- .../MusicLibraryPage.xaml | 9 ++++++ .../Adapters/VirtualListViewAdapterBase.cs | 7 ++++- VirtualListView/PositionalViewSelector.cs | 30 ++++++++++++++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/Sample/VirtualListViewSample/MusicLibraryPage.xaml b/Sample/VirtualListViewSample/MusicLibraryPage.xaml index 45ad698..508391c 100644 --- a/Sample/VirtualListViewSample/MusicLibraryPage.xaml +++ b/Sample/VirtualListViewSample/MusicLibraryPage.xaml @@ -72,6 +72,15 @@