diff --git a/components/AdvancedCollectionView/OpenSolution.bat b/components/AdvancedCollectionView/OpenSolution.bat
new file mode 100644
index 00000000..814a56d4
--- /dev/null
+++ b/components/AdvancedCollectionView/OpenSolution.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
\ No newline at end of file
diff --git a/components/AdvancedCollectionView/samples/AdvancedCollectionView.Samples.csproj b/components/AdvancedCollectionView/samples/AdvancedCollectionView.Samples.csproj
new file mode 100644
index 00000000..6469f7cf
--- /dev/null
+++ b/components/AdvancedCollectionView/samples/AdvancedCollectionView.Samples.csproj
@@ -0,0 +1,8 @@
+
+
+ AdvancedCollectionView
+
+
+
+
+
diff --git a/components/AdvancedCollectionView/samples/AdvancedCollectionView.md b/components/AdvancedCollectionView/samples/AdvancedCollectionView.md
new file mode 100644
index 00000000..c3dfd5da
--- /dev/null
+++ b/components/AdvancedCollectionView/samples/AdvancedCollectionView.md
@@ -0,0 +1,152 @@
+---
+title: AdvancedCollectionView
+author: nmetulev
+description: The AdvancedCollectionView is a collection view implementation that support filtering, sorting and incremental loading. It's meant to be used in a viewmodel.
+keywords: AdvancedCollectionView, data, sorting, filtering
+dev_langs:
+ - csharp
+category: Controls
+subcategory: Layout
+discussion-id: 0
+issue-id: 0
+---
+
+# AdvancedCollectionView
+
+The [AdvancedCollectionView](/dotnet/api/microsoft.toolkit.uwp.ui.advancedcollectionview) is a collection view implementation that support filtering, sorting and incremental loading. It's meant to be used in a viewmodel.
+
+> [!Sample AdvancedCollectionViewSample]
+
+## Usage
+
+In your viewmodel instead of having a public [IEnumerable](/dotnet/core/api/system.collections.generic.ienumerable-1) of some sort to be bound to an eg. [Listview](/uwp/api/Windows.UI.Xaml.Controls.ListView), create a public AdvancedCollectionView and pass your list in the constructor to it. If you've done that you can use the many useful features it provides:
+
+* sorting your list using the `SortDirection` helper: specify any number of property names to sort on with the direction desired
+* filtering your list using a [Predicate](/dotnet/core/api/system.predicate-1): this will automatically filter your list only to the items that pass the check by the predicate provided
+* deferring notifications using the `NotificationDeferrer` helper: with a convenient _using_ pattern you can increase performance while doing large-scale modifications in your list by waiting with updates until you've completed your work
+* incremental loading: if your source collection supports the feature then AdvancedCollectionView will do as well (it simply forwards the calls)
+* live shaping: when constructing the `AdvancedCollectionView` you may specify that the collection use live shaping. This means that the collection will re-filter or re-sort if there are changes to the sort properties or filter properties that are specified using `ObserveFilterProperty`
+
+## Example
+
+```csharp
+using Microsoft.Toolkit.Uwp.UI;
+
+// Grab a sample type
+public class Person
+{
+ public string Name { get; set; }
+}
+
+// Set up the original list with a few sample items
+var oc = new ObservableCollection
+{
+ new Person { Name = "Staff" },
+ new Person { Name = "42" },
+ new Person { Name = "Swan" },
+ new Person { Name = "Orchid" },
+ new Person { Name = "15" },
+ new Person { Name = "Flame" },
+ new Person { Name = "16" },
+ new Person { Name = "Arrow" },
+ new Person { Name = "Tempest" },
+ new Person { Name = "23" },
+ new Person { Name = "Pearl" },
+ new Person { Name = "Hydra" },
+ new Person { Name = "Lamp Post" },
+ new Person { Name = "4" },
+ new Person { Name = "Looking Glass" },
+ new Person { Name = "8" },
+};
+
+// Set up the AdvancedCollectionView with live shaping enabled to filter and sort the original list
+var acv = new AdvancedCollectionView(oc, true);
+
+// Let's filter out the integers
+int nul;
+acv.Filter = x => !int.TryParse(((Person)x).Name, out nul);
+
+// And sort ascending by the property "Name"
+acv.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending));
+
+// Let's add a Person to the observable collection
+var person = new Person { Name = "Aardvark" };
+oc.Add(person);
+
+// Our added person is now at the top of the list, but if we rename this person, we can trigger a re-sort
+person.Name = "Zaphod"; // Now a re-sort is triggered and person will be last in the list
+
+// AdvancedCollectionView can be bound to anything that uses collections.
+YourListView.ItemsSource = acv;
+```
+
+## Properties
+
+| Property | Type | Description |
+| -- | -- | -- |
+| CanFilter | bool | Gets a value indicating whether this CollectionView can filter its items |
+| CanSort | bool | Gets a value indicating whether this CollectionView can sort its items |
+| CollectionGroups | IObservableVector\ | Gets the groups in collection |
+| Count | int | Get the count of items |
+| CurrentItem | object | Gets or sets the current item |
+| CurrentPosition | int | Gets the position of current item |
+| Filter | Predicate\ | Gets or sets the predicate used to filter the visible items |
+| HasMoreItems | bool | Gets a value indicating whether the source has more items |
+| IsCurrentAfterLast | bool | Gets a value indicating whether the current item is after the last visible item |
+| IsCurrentBeforeFirst | bool | Gets a value indicating whether the current item is before the first visible item |
+| IsReadOnly | bool | Get a value indicating whether this CollectionView is read only |
+| SortDescriptions | IList<[SortDescription](/dotnet/api/microsoft.toolkit.uwp.ui.sortdescription)> | Gets SortDescriptions to sort the visible items |
+| Source | IEnumerable | Gets or sets the source |
+| SourceCollection | IEnumerable | Gets the source collection |
+| this[int] | int | Gets or sets the element at the specified index |
+
+## Methods
+
+| Methods | Return Type | Description |
+| -- | -- | -- |
+| Add(Object) | void | Add item |
+| Clear() | void | Clear item |
+| Contains(Object) | bool | Returns `true` if the given item contained in CollectionView |
+| B(float, string) | int | Description |
+| DeferRefresh() | IDisposable | Stops refreshing until it is disposed |
+| IndexOf(Object) | int | Return index of an item |
+| Insert(Int32, Object) | void | Insert an item in a particular place |
+| LoadMoreItemsAsync(UInt32) | IAsyncOperation<[LoadMoreItemsResult](/uwp/api/Windows.UI.Xaml.Data.LoadMoreItemsResult)> | Load more items from the source |
+| MoveCurrentTo(Object) | bool | Move current index to item. Returns success of operation |
+| MoveCurrentToFirst() | bool | Move current item to first item. Returns success of operation |
+| MoveCurrentToLast() | bool | Move current item to last item. Returns success of operation |
+| MoveCurrentToNext() | bool | Move current item to next item |
+| MoveCurrentToPosition(Int32) | bool | Moves selected item to position |
+| MoveCurrentToPrevious() | bool | Move current item to previous item |
+| Refresh() | void | Manually refresh the view |
+| Remove(Object) | bool | Remove item |
+| RemoveAt(Int32) | bool | Remove item with index |
+
+## Events
+
+| Events | Description |
+| -- | -- |
+| CurrentChanged | Current item changed event handler |
+| CurrentChanging | Current item changing event handler |
+| PropertyChanged | Occurs when a property value changes |
+| VectorChanged | Occurs when the vector changes |
+
+## Remarks
+
+_What source can I use?_
+
+It's not necessary to use an eg. [ObservableCollection](/dotnet/core/api/system.collections.objectmodel.observablecollection-1) to use the AdvancedCollectionView. It works as expected even when providing a simple [List](/dotnet/core/api/system.collections.generic.list-1) in the constructor.
+
+_Any performance guidelines?_
+
+If you're removing, modifying or inserting large amounts of items while having filtering and/or sorting set up, it's recommended that you use the `NotificationDeferrer` helper provided. It skips any performance heavy logic while it's in use, and automatically calls the `Refresh` method when disposed.
+
+```csharp
+using (acv.DeferRefresh())
+{
+ for (var i = 0; i < 500; i++)
+ {
+ acv.Add(new Person { Name = "defer" });
+ }
+} // acv.Refresh() gets called here
+```
diff --git a/components/AdvancedCollectionView/samples/AdvancedCollectionViewSample.cs b/components/AdvancedCollectionView/samples/AdvancedCollectionViewSample.cs
new file mode 100644
index 00000000..b9115d59
--- /dev/null
+++ b/components/AdvancedCollectionView/samples/AdvancedCollectionViewSample.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI;
+
+namespace AdvancedCollectionViewExperiment.Samples;
+
+[ToolkitSample(id: nameof(AdvancedCollectionViewSample), "AdvancedCollectionView", description: $"A sample for showing how to create and use a {nameof(AdvancedCollectionView)}.")]
+public sealed partial class AdvancedCollectionViewSample : Page
+{
+ public ObservableCollection? oc;
+
+ public AdvancedCollectionViewSample()
+ {
+ this.InitializeComponent();
+ Setup();
+ }
+
+ private void Setup()
+ {
+ // left list
+ oc = new ObservableCollection
+ {
+ new Person { Name = "Staff" },
+ new Person { Name = "42" },
+ new Person { Name = "Swan" },
+ new Person { Name = "Orchid" },
+ new Person { Name = "15" },
+ new Person { Name = "Flame" },
+ new Person { Name = "16" },
+ new Person { Name = "Arrow" },
+ new Person { Name = "Tempest" },
+ new Person { Name = "23" },
+ new Person { Name = "Pearl" },
+ new Person { Name = "Hydra" },
+ new Person { Name = "Lamp Post" },
+ new Person { Name = "4" },
+ new Person { Name = "Looking Glass" },
+ new Person { Name = "8" },
+ };
+
+ LeftList.ItemsSource = oc;
+
+ // right list
+ var acv = new AdvancedCollectionView(oc);
+ int nul;
+ acv.Filter = x => !int.TryParse(((Person)x).Name, out nul);
+ acv.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending));
+
+ RightList.ItemsSource = acv;
+ }
+
+ private void Add_Click(object sender, RoutedEventArgs e)
+ {
+ if (!string.IsNullOrWhiteSpace(NewItemBox.Text))
+ {
+ oc!.Insert(0, new Person { Name = NewItemBox.Text });
+ NewItemBox.Text = "";
+ }
+ }
+
+ ///
+ /// A sample class used to show how to use the interface.
+ ///
+ public class Person
+ {
+ ///
+ /// Gets or sets the name of the person.
+ ///
+ public string? Name { get; set; }
+ }
+}
diff --git a/components/AdvancedCollectionView/samples/AdvancedCollectionViewSample.xaml b/components/AdvancedCollectionView/samples/AdvancedCollectionViewSample.xaml
new file mode 100644
index 00000000..15060922
--- /dev/null
+++ b/components/AdvancedCollectionView/samples/AdvancedCollectionViewSample.xaml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/AdvancedCollectionView/samples/Dependencies.props b/components/AdvancedCollectionView/samples/Dependencies.props
new file mode 100644
index 00000000..e622e1df
--- /dev/null
+++ b/components/AdvancedCollectionView/samples/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/AdvancedCollectionView/src/AdditionalAssemblyInfo.cs b/components/AdvancedCollectionView/src/AdditionalAssemblyInfo.cs
new file mode 100644
index 00000000..5f8cb7e3
--- /dev/null
+++ b/components/AdvancedCollectionView/src/AdditionalAssemblyInfo.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+// These `InternalsVisibleTo` calls are intended to make it easier for
+// for any internal code to be testable in all the different test projects
+// used with the Labs infrastructure.
+[assembly: InternalsVisibleTo("AdvancedCollectionView.Tests.Uwp")]
+[assembly: InternalsVisibleTo("AdvancedCollectionView.Tests.WinAppSdk")]
+[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")]
+[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")]
diff --git a/components/AdvancedCollectionView/src/AdvancedCollectionView.Defer.cs b/components/AdvancedCollectionView/src/AdvancedCollectionView.Defer.cs
new file mode 100644
index 00000000..3f745d60
--- /dev/null
+++ b/components/AdvancedCollectionView/src/AdvancedCollectionView.Defer.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// A collection view implementation that supports filtering, grouping, sorting and incremental loading
+///
+public partial class AdvancedCollectionView
+{
+ ///
+ /// Stops refreshing until it is disposed
+ ///
+ /// An disposable object
+ public IDisposable DeferRefresh()
+ {
+ return new NotificationDeferrer(this);
+ }
+
+ ///
+ /// Notification deferrer helper class
+ ///
+#pragma warning disable CA1063 // Implement IDisposable Correctly
+ public class NotificationDeferrer : IDisposable
+#pragma warning restore CA1063 // Implement IDisposable Correctly
+ {
+ private readonly AdvancedCollectionView _acvs;
+ private readonly object _currentItem;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Source ACVS
+ public NotificationDeferrer(AdvancedCollectionView acvs)
+ {
+ _acvs = acvs;
+ _currentItem = _acvs.CurrentItem;
+ _acvs._deferCounter++;
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ /// 2
+#pragma warning disable CA1063 // Implement IDisposable Correctly
+ public void Dispose()
+#pragma warning restore CA1063 // Implement IDisposable Correctly
+ {
+ _acvs.MoveCurrentTo(_currentItem);
+ _acvs._deferCounter--;
+ _acvs.Refresh();
+ }
+ }
+}
diff --git a/components/AdvancedCollectionView/src/AdvancedCollectionView.Events.cs b/components/AdvancedCollectionView/src/AdvancedCollectionView.Events.cs
new file mode 100644
index 00000000..e6c36111
--- /dev/null
+++ b/components/AdvancedCollectionView/src/AdvancedCollectionView.Events.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// A collection view implementation that supports filtering, grouping, sorting and incremental loading
+///
+public partial class AdvancedCollectionView
+{
+ ///
+ /// Currently selected item changing event
+ ///
+ /// event args
+ private void OnCurrentChanging(CurrentChangingEventArgs e)
+ {
+ if (_deferCounter > 0)
+ {
+ return;
+ }
+
+ CurrentChanging?.Invoke(this, e);
+ }
+
+ ///
+ /// Currently selected item changed event
+ ///
+ /// event args
+ private void OnCurrentChanged(object e)
+ {
+ if (_deferCounter > 0)
+ {
+ return;
+ }
+
+ CurrentChanged?.Invoke(this, e);
+
+ // ReSharper disable once ExplicitCallerInfoArgument
+ OnPropertyChanged(nameof(CurrentItem));
+ }
+
+ ///
+ /// Vector changed event
+ ///
+ /// event args
+ private void OnVectorChanged(IVectorChangedEventArgs e)
+ {
+ if (_deferCounter > 0)
+ {
+ return;
+ }
+
+ VectorChanged?.Invoke(this, e);
+
+ // ReSharper disable once ExplicitCallerInfoArgument
+ OnPropertyChanged(nameof(Count));
+ }
+}
diff --git a/components/AdvancedCollectionView/src/AdvancedCollectionView.cs b/components/AdvancedCollectionView/src/AdvancedCollectionView.cs
new file mode 100644
index 00000000..b7856473
--- /dev/null
+++ b/components/AdvancedCollectionView/src/AdvancedCollectionView.cs
@@ -0,0 +1,799 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Helpers;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Runtime.CompilerServices;
+using NotifyCollectionChangedAction = global::System.Collections.Specialized.NotifyCollectionChangedAction;
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// A collection view implementation that supports filtering, sorting and incremental loading
+///
+public partial class AdvancedCollectionView : IAdvancedCollectionView, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer
+{
+ private readonly List _view;
+
+ private readonly ObservableCollection _sortDescriptions;
+
+ private readonly Dictionary _sortProperties;
+
+ private readonly bool _liveShapingEnabled;
+
+ private readonly HashSet _observedFilterProperties = new HashSet();
+
+ private IList _source;
+
+ private Predicate _filter;
+ private int _deferCounter;
+
+ private WeakEventListener _sourceWeakEventListener;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AdvancedCollectionView()
+ : this(new List(0))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// source IEnumerable
+ /// Denotes whether or not this ACV should re-filter/re-sort if a PropertyChanged is raised for an observed property.
+#pragma warning disable CS8767
+#pragma warning disable CS8769
+#pragma warning disable CS8622
+#pragma warning disable CS8600
+#pragma warning disable CS8601
+#pragma warning disable CS8604
+#pragma warning disable CS8603 // Possible null reference return.
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+ public AdvancedCollectionView(IList source, bool isLiveShaping = false)
+ {
+ _liveShapingEnabled = isLiveShaping;
+ _view = new List();
+ _sortDescriptions = new ObservableCollection();
+ _sortDescriptions.CollectionChanged += SortDescriptions_CollectionChanged;
+ _sortProperties = new Dictionary();
+ Source = source;
+ }
+
+ ///
+ /// Gets or sets the source
+ ///
+ public IList Source
+ {
+ get
+ {
+ return _source;
+ }
+
+ set
+ {
+ // ReSharper disable once PossibleUnintendedReferenceComparison
+ if (_source == value)
+ {
+ return;
+ }
+
+ if (_source != null)
+ {
+ DetachPropertyChangedHandler(_source);
+ }
+
+ _source = value;
+ AttachPropertyChangedHandler(_source);
+
+ _sourceWeakEventListener?.Detach();
+
+ if (_source is INotifyCollectionChanged sourceNcc)
+ {
+ _sourceWeakEventListener =
+ new WeakEventListener(this)
+ {
+ // Call the actual collection changed event
+ OnEventAction = (source, changed, arg3) => SourceNcc_CollectionChanged(source, arg3),
+
+ // The source doesn't exist anymore
+ OnDetachAction = (listener) => sourceNcc.CollectionChanged -= _sourceWeakEventListener!.OnEvent
+ };
+ sourceNcc.CollectionChanged += _sourceWeakEventListener.OnEvent;
+ }
+
+ HandleSourceChanged();
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Manually refresh the view
+ ///
+ public void Refresh()
+ {
+ HandleSourceChanged();
+ }
+
+ ///
+ public void RefreshFilter()
+ {
+ HandleFilterChanged();
+ }
+
+ ///
+ public void RefreshSorting()
+ {
+ HandleSortChanged();
+ }
+
+ ///
+ public IEnumerator GetEnumerator() => _view.GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => _view.GetEnumerator();
+
+ ///
+ public void Add(object item)
+ {
+ if (IsReadOnly)
+ {
+ throw new NotSupportedException("Collection is read-only.");
+ }
+
+ _source.Add(item);
+ }
+
+ ///
+ public void Clear()
+ {
+ if (IsReadOnly)
+ {
+ throw new NotSupportedException("Collection is read-only.");
+ }
+
+ _source.Clear();
+ }
+
+ ///
+ public bool Contains(object item) => _view.Contains(item);
+
+ ///
+ public void CopyTo(object[] array, int arrayIndex) => _view.CopyTo(array, arrayIndex);
+
+ ///
+ public bool Remove(object item)
+ {
+ if (IsReadOnly)
+ {
+ throw new NotSupportedException("Collection is read-only.");
+ }
+
+ _source.Remove(item);
+ return true;
+ }
+
+ ///
+ public int Count => _view.Count;
+
+ ///
+ public bool IsReadOnly => _source == null || _source.IsReadOnly;
+
+ ///
+ public int IndexOf(object item) => _view.IndexOf(item);
+
+ ///
+ public void Insert(int index, object item)
+ {
+ if (IsReadOnly)
+ {
+ throw new NotSupportedException("Collection is read-only.");
+ }
+
+ _source.Insert(index, item);
+ }
+
+ ///
+ /// Removes the item at the specified index.
+ ///
+ /// The zero-based index of the item to remove. is not a valid index in the .The is read-only.
+ public void RemoveAt(int index) => Remove(_view[index]);
+
+ ///
+ /// Gets or sets the element at the specified index.
+ ///
+ ///
+ /// The element at the specified index.
+ ///
+ /// The zero-based index of the element to get or set. is not a valid index in the .The property is set and the is read-only.
+ public object this[int index]
+ {
+ get { return _view[index]; }
+ set { _view[index] = value; }
+ }
+
+ ///
+ /// Occurs when the vector changes.
+ ///
+ public event Windows.Foundation.Collections.VectorChangedEventHandler VectorChanged;
+
+ ///
+ /// Move current index to item
+ ///
+ /// item
+ /// success of operation
+ public bool MoveCurrentTo(object item) => item == CurrentItem || MoveCurrentToIndex(IndexOf(item));
+
+ ///
+ /// Moves selected item to position
+ ///
+ /// index
+ /// success of operation
+ public bool MoveCurrentToPosition(int index) => MoveCurrentToIndex(index);
+
+ ///
+ /// Move current item to first item
+ ///
+ /// success of operation
+ public bool MoveCurrentToFirst() => MoveCurrentToIndex(0);
+
+ ///
+ /// Move current item to last item
+ ///
+ /// success of operation
+ public bool MoveCurrentToLast() => MoveCurrentToIndex(_view.Count - 1);
+
+ ///
+ /// Move current item to next item
+ ///
+ /// success of operation
+ public bool MoveCurrentToNext() => MoveCurrentToIndex(CurrentPosition + 1);
+
+ ///
+ /// Move current item to previous item
+ ///
+ /// success of operation
+ public bool MoveCurrentToPrevious() => MoveCurrentToIndex(CurrentPosition - 1);
+
+ ///
+ /// Load more items from the source
+ ///
+ /// number of items to load
+ /// Async operation of LoadMoreItemsResult
+ /// Not implemented yet...
+ public IAsyncOperation LoadMoreItemsAsync(uint count)
+ {
+ var sil = _source as ISupportIncrementalLoading;
+ return sil?.LoadMoreItemsAsync(count);
+ }
+
+ ///
+ /// Gets the groups in collection
+ ///
+ public IObservableVector CollectionGroups => null;
+
+ ///
+ /// Gets or sets the current item
+ ///
+ public object CurrentItem
+ {
+
+ get { return CurrentPosition > -1 && CurrentPosition < _view.Count ? _view[CurrentPosition] : null; }
+#pragma warning restore CS8603 // Possible null reference return.
+ set { MoveCurrentTo(value); }
+ }
+
+ ///
+ /// Gets the position of current item
+ ///
+ public int CurrentPosition { get; private set; }
+
+ ///
+ /// Gets a value indicating whether the source has more items
+ ///
+ public bool HasMoreItems => (_source as ISupportIncrementalLoading)?.HasMoreItems ?? false;
+
+ ///
+ /// Gets a value indicating whether the current item is after the last visible item
+ ///
+ public bool IsCurrentAfterLast => CurrentPosition >= _view.Count;
+
+ ///
+ /// Gets a value indicating whether the current item is before the first visible item
+ ///
+ public bool IsCurrentBeforeFirst => CurrentPosition < 0;
+
+ ///
+ /// Current item changed event handler
+ ///
+ public event EventHandler CurrentChanged;
+
+ ///
+ /// Current item changing event handler
+ ///
+ public event CurrentChangingEventHandler CurrentChanging;
+
+ ///
+ /// Gets a value indicating whether this CollectionView can filter its items
+ ///
+ public bool CanFilter => true;
+
+ ///
+ /// Gets or sets the predicate used to filter the visible items
+ ///
+ public Predicate Filter
+ {
+ get
+ {
+ return _filter;
+ }
+
+ set
+ {
+ if (_filter == value)
+ {
+ return;
+ }
+
+ _filter = value;
+ HandleFilterChanged();
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether this CollectionView can sort its items
+ ///
+ public bool CanSort => true;
+
+ ///
+ /// Gets SortDescriptions to sort the visible items
+ ///
+ public IList SortDescriptions => _sortDescriptions;
+
+ /*
+ ///
+ /// Gets a value indicating whether this CollectionView can group its items
+ ///
+ public bool CanGroup => false;
+
+ ///
+ /// Gets GroupDescriptions to group the visible items
+ ///
+ public IList GroupDescriptions => null;
+ */
+
+ ///
+ /// Gets the source collection
+ ///
+ public IEnumerable SourceCollection => _source;
+
+ ///
+ /// IComparer implementation
+ ///
+ /// Object A
+ /// Object B
+ /// Comparison value
+#pragma warning disable CA1033 // Interface methods should be callable by child types
+ int IComparer.Compare(object x, object y)
+#pragma warning restore CA1033 // Interface methods should be callable by child types
+ {
+ if (!_sortProperties.Any())
+ {
+ var type = x.GetType();
+ foreach (var sd in _sortDescriptions)
+ {
+ if (!string.IsNullOrEmpty(sd.PropertyName))
+ {
+ _sortProperties[sd.PropertyName] = type.GetProperty(sd.PropertyName);
+ }
+ }
+ }
+
+ foreach (var sd in _sortDescriptions)
+ {
+ object cx, cy;
+
+ if (string.IsNullOrEmpty(sd.PropertyName))
+ {
+ cx = x;
+ cy = y;
+ }
+ else
+ {
+ var pi = _sortProperties[sd.PropertyName];
+
+ cx = pi.GetValue(x!);
+ cy = pi.GetValue(y!);
+ }
+
+ var cmp = sd.Comparer.Compare(cx, cy);
+
+ if (cmp != 0)
+ {
+ return sd.Direction == SortDirection.Ascending ? +cmp : -cmp;
+ }
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Occurs when a property value changes.
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ ///
+ /// Property changed event invoker
+ ///
+ /// name of the property that changed
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null!)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ ///
+ public void ObserveFilterProperty(string propertyName)
+ {
+ _observedFilterProperties.Add(propertyName);
+ }
+
+ ///
+ public void ClearObservedFilterProperties()
+ {
+ _observedFilterProperties.Clear();
+ }
+
+ private void ItemOnPropertyChanged(object item, PropertyChangedEventArgs e)
+ {
+ if (!_liveShapingEnabled)
+ {
+ return;
+ }
+
+ var filterResult = _filter?.Invoke(item);
+
+ if (filterResult.HasValue && _observedFilterProperties.Contains(e.PropertyName))
+ {
+ var viewIndex = _view.IndexOf(item);
+ if (viewIndex != -1 && !filterResult.Value)
+ {
+ RemoveFromView(viewIndex, item);
+ }
+ else if (viewIndex == -1 && filterResult.Value)
+ {
+ var index = _source.IndexOf(item);
+ HandleItemAdded(index, item);
+ }
+ }
+
+ if ((filterResult ?? true) && SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName))
+ {
+ var oldIndex = _view.IndexOf(item);
+
+ // Check if item is in view:
+ if (oldIndex < 0)
+ {
+ return;
+ }
+
+ _view.RemoveAt(oldIndex);
+ var targetIndex = _view.BinarySearch(item, this);
+ if (targetIndex < 0)
+ {
+ targetIndex = ~targetIndex;
+ }
+
+ // Only trigger expensive UI updates if the index really changed:
+ if (targetIndex != oldIndex)
+ {
+ OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, oldIndex, item));
+
+ _view.Insert(targetIndex, item);
+
+ OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, targetIndex, item));
+ }
+ else
+ {
+ _view.Insert(targetIndex, item);
+ }
+ }
+ else if (string.IsNullOrEmpty(e.PropertyName))
+ {
+ HandleSourceChanged();
+ }
+ }
+
+ private void AttachPropertyChangedHandler(IEnumerable items)
+ {
+ if (!_liveShapingEnabled || items == null)
+ {
+ return;
+ }
+
+ foreach (var item in items.OfType())
+ {
+ item.PropertyChanged += ItemOnPropertyChanged;
+ }
+ }
+
+ private void DetachPropertyChangedHandler(IEnumerable items)
+ {
+ if (!_liveShapingEnabled || items == null)
+ {
+ return;
+ }
+
+ foreach (var item in items.OfType())
+ {
+ item.PropertyChanged -= ItemOnPropertyChanged;
+ }
+ }
+
+ private void HandleSortChanged()
+ {
+ _sortProperties.Clear();
+ _view.Sort(this);
+ _sortProperties.Clear();
+ OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
+ }
+
+ private void HandleFilterChanged()
+ {
+ if (_filter != null)
+ {
+ for (var index = 0; index < _view.Count; index++)
+ {
+ var item = _view.ElementAt(index);
+ if (_filter(item))
+ {
+ continue;
+ }
+
+ RemoveFromView(index, item);
+ index--;
+ }
+ }
+
+ var viewHash = new HashSet(_view);
+ var viewIndex = 0;
+ for (var index = 0; index < _source.Count; index++)
+ {
+ var item = _source[index];
+ if (viewHash.Contains(item))
+ {
+ viewIndex++;
+ continue;
+ }
+
+ if (HandleItemAdded(index, item, viewIndex))
+ {
+ viewIndex++;
+ }
+ }
+ }
+
+ private void HandleSourceChanged()
+ {
+ _sortProperties.Clear();
+ var currentItem = CurrentItem;
+ _view.Clear();
+ foreach (var item in Source)
+ {
+ if (_filter != null && !_filter(item))
+ {
+ continue;
+ }
+
+ if (_sortDescriptions.Any())
+ {
+ var targetIndex = _view.BinarySearch(item, this);
+ if (targetIndex < 0)
+ {
+ targetIndex = ~targetIndex;
+ }
+
+ _view.Insert(targetIndex, item);
+ }
+ else
+ {
+ _view.Add(item);
+ }
+ }
+
+ _sortProperties.Clear();
+ OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
+ MoveCurrentTo(currentItem);
+ }
+
+ private void SourceNcc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ AttachPropertyChangedHandler(e.NewItems);
+ if (_deferCounter <= 0)
+ {
+ if (e.NewItems?.Count == 1)
+ {
+ HandleItemAdded(e.NewStartingIndex, e.NewItems[0]);
+ }
+ else
+ {
+ HandleSourceChanged();
+ }
+ }
+
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ DetachPropertyChangedHandler(e.OldItems);
+ if (_deferCounter <= 0)
+ {
+ if (e.OldItems?.Count == 1)
+ {
+ HandleItemRemoved(e.OldStartingIndex, e.OldItems[0]);
+ }
+ else
+ {
+ HandleSourceChanged();
+ }
+ }
+
+ break;
+ case NotifyCollectionChangedAction.Move:
+ case NotifyCollectionChangedAction.Replace:
+ case NotifyCollectionChangedAction.Reset:
+ if (_deferCounter <= 0)
+ {
+ HandleSourceChanged();
+ }
+
+ break;
+ }
+ }
+
+ private bool HandleItemAdded(int newStartingIndex, object newItem, int? viewIndex = null)
+ {
+ if (_filter != null && !_filter(newItem))
+ {
+ return false;
+ }
+
+ var newViewIndex = _view.Count;
+
+ if (_sortDescriptions.Any())
+ {
+ _sortProperties.Clear();
+ newViewIndex = _view.BinarySearch(newItem, this);
+ if (newViewIndex < 0)
+ {
+ newViewIndex = ~newViewIndex;
+ }
+ }
+ else if (_filter != null)
+ {
+ if (_source == null)
+ {
+ HandleSourceChanged();
+ return false;
+ }
+
+ if (newStartingIndex == 0 || _view.Count == 0)
+ {
+ newViewIndex = 0;
+ }
+ else if (newStartingIndex == _source.Count - 1)
+ {
+ newViewIndex = _view.Count - 1;
+ }
+ else if (viewIndex.HasValue)
+ {
+ newViewIndex = viewIndex.Value;
+ }
+ else
+ {
+ for (int i = 0, j = 0; i < _source.Count; i++)
+ {
+ if (i == newStartingIndex)
+ {
+ newViewIndex = j;
+ break;
+ }
+
+ if (_view[j] == _source[i])
+ {
+ j++;
+ }
+ }
+ }
+ }
+
+ _view.Insert(newViewIndex, newItem);
+ if (newViewIndex <= CurrentPosition)
+ {
+ CurrentPosition++;
+ }
+
+ var e = new VectorChangedEventArgs(CollectionChange.ItemInserted, newViewIndex, newItem);
+ OnVectorChanged(e);
+ return true;
+ }
+
+ private void HandleItemRemoved(int oldStartingIndex, object oldItem)
+ {
+ if (_filter != null && !_filter(oldItem))
+ {
+ return;
+ }
+
+ if (oldStartingIndex < 0 || oldStartingIndex >= _view.Count || !Equals(_view[oldStartingIndex], oldItem))
+ {
+ oldStartingIndex = _view.IndexOf(oldItem);
+ }
+
+ if (oldStartingIndex < 0)
+ {
+ return;
+ }
+
+ RemoveFromView(oldStartingIndex, oldItem);
+ }
+
+ private void RemoveFromView(int itemIndex, object item)
+ {
+ _view.RemoveAt(itemIndex);
+ if (itemIndex <= CurrentPosition)
+ {
+ CurrentPosition--;
+ }
+
+ var e = new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item);
+ OnVectorChanged(e);
+ }
+
+ private void SortDescriptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (_deferCounter > 0)
+ {
+ return;
+ }
+
+ HandleSortChanged();
+ }
+
+ private bool MoveCurrentToIndex(int i)
+ {
+ if (i < -1 || i >= _view.Count)
+ {
+ return false;
+ }
+
+ if (i == CurrentPosition)
+ {
+ return false;
+ }
+
+ var e = new CurrentChangingEventArgs();
+ OnCurrentChanging(e);
+ if (e.Cancel)
+ {
+ return false;
+ }
+
+ CurrentPosition = i;
+ OnCurrentChanged(null!);
+ return true;
+ }
+}
+
+#pragma warning restore CS8767
+#pragma warning restore CS8769
+#pragma warning restore CS8622
+#pragma warning restore CS8601
+#pragma warning restore CS8600
+#pragma warning restore CS8604
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+#pragma warning restore CS8603 // Possible null reference return.
diff --git a/components/AdvancedCollectionView/src/CommunityToolkit.WinUI.AdvancedCollectionView.csproj b/components/AdvancedCollectionView/src/CommunityToolkit.WinUI.AdvancedCollectionView.csproj
new file mode 100644
index 00000000..91426b29
--- /dev/null
+++ b/components/AdvancedCollectionView/src/CommunityToolkit.WinUI.AdvancedCollectionView.csproj
@@ -0,0 +1,17 @@
+
+
+ AdvancedCollectionView
+ This package contains AdvancedCollectionView.
+ 8.0.0-beta.1
+
+
+ CommunityToolkit.WinUI.AdvancedCollectionViewRns
+
+
+
+
+
+
+
+
+
diff --git a/components/AdvancedCollectionView/src/Dependencies.props b/components/AdvancedCollectionView/src/Dependencies.props
new file mode 100644
index 00000000..e622e1df
--- /dev/null
+++ b/components/AdvancedCollectionView/src/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/AdvancedCollectionView/src/IAdvancedCollectionView.cs b/components/AdvancedCollectionView/src/IAdvancedCollectionView.cs
new file mode 100644
index 00000000..35050b27
--- /dev/null
+++ b/components/AdvancedCollectionView/src/IAdvancedCollectionView.cs
@@ -0,0 +1,81 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections;
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// Extended ICollectionView with filtering and sorting
+///
+public interface IAdvancedCollectionView : ICollectionView
+{
+ ///
+ /// Gets a value indicating whether this CollectionView can filter its items
+ ///
+ bool CanFilter { get; }
+
+ ///
+ /// Gets or sets the predicate used to filter the visible items
+ ///
+ Predicate Filter { get; set; }
+
+ ///
+ /// Gets a value indicating whether this CollectionView can sort its items
+ ///
+ bool CanSort { get; }
+
+ ///
+ /// Gets SortDescriptions to sort the visible items
+ ///
+ IList SortDescriptions { get; }
+
+ /*
+ ///
+ /// Gets a value indicating whether this CollectionView can group its items
+ ///
+ bool CanGroup { get; }
+
+ ///
+ /// Gets GroupDescriptions to group the visible items
+ ///
+ IList GroupDescriptions { get; }
+ */
+
+ ///
+ /// Gets the source collection
+ ///
+ IEnumerable SourceCollection { get; }
+
+ ///
+ /// Stops refreshing until it is disposed
+ ///
+ /// An disposable object
+ IDisposable DeferRefresh();
+
+ ///
+ /// Manually refreshes the view
+ ///
+ void Refresh();
+
+ ///
+ /// Manually refreshes the filter on the view
+ ///
+ void RefreshFilter();
+
+ ///
+ /// Manually refreshes the sorting on the view
+ ///
+ void RefreshSorting();
+
+ ///
+ /// Add a property to re-filter an item on when it is changed
+ ///
+ void ObserveFilterProperty(string propertyName);
+
+ ///
+ /// Clears all properties items are re-filtered on
+ ///
+ void ClearObservedFilterProperties();
+}
diff --git a/components/AdvancedCollectionView/src/MultiTarget.props b/components/AdvancedCollectionView/src/MultiTarget.props
new file mode 100644
index 00000000..b11c1942
--- /dev/null
+++ b/components/AdvancedCollectionView/src/MultiTarget.props
@@ -0,0 +1,9 @@
+
+
+
+ uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android;
+
+
\ No newline at end of file
diff --git a/components/AdvancedCollectionView/src/SortDescription.cs b/components/AdvancedCollectionView/src/SortDescription.cs
new file mode 100644
index 00000000..dc1dd167
--- /dev/null
+++ b/components/AdvancedCollectionView/src/SortDescription.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections;
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// Sort description
+///
+public class SortDescription
+{
+ ///
+ /// Gets the name of property to sort on
+ ///
+ public string PropertyName { get; }
+
+ ///
+ /// Gets the direction of sort
+ ///
+ public SortDirection Direction { get; }
+
+ ///
+ /// Gets the comparer
+ ///
+ public IComparer Comparer { get; }
+
+ ///
+ /// Initializes a new instance of the class that describes
+ /// a sort on the object itself
+ ///
+ /// Direction of sort
+ /// Comparer to use. If null, will use default comparer
+ public SortDescription(SortDirection direction, IComparer? comparer = null)
+ : this(null!, direction, comparer!)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Name of property to sort on
+ /// Direction of sort
+ /// Comparer to use. If null, will use default comparer
+ public SortDescription(string propertyName, SortDirection direction, IComparer? comparer = null)
+ {
+ PropertyName = propertyName;
+ Direction = direction;
+ Comparer = comparer ?? ObjectComparer.Instance;
+ }
+
+ private class ObjectComparer : IComparer
+ {
+ public static readonly IComparer Instance = new ObjectComparer();
+
+ private ObjectComparer()
+ {
+ }
+
+ public int Compare(object? x, object? y)
+ {
+ var cx = x as IComparable;
+ var cy = y as IComparable;
+
+ // ReSharper disable once PossibleUnintendedReferenceComparison
+ return cx == cy ? 0 : cx == null ? -1 : cy == null ? +1 : cx.CompareTo(cy);
+ }
+ }
+}
diff --git a/components/AdvancedCollectionView/src/SortDirection.cs b/components/AdvancedCollectionView/src/SortDirection.cs
new file mode 100644
index 00000000..38a9d8f0
--- /dev/null
+++ b/components/AdvancedCollectionView/src/SortDirection.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// Sort direction enum
+///
+public enum SortDirection
+{
+ ///
+ /// Ascending order (eg. abc...)
+ ///
+ Ascending = 0,
+
+ ///
+ /// Descending order (eg. zyx...)
+ ///
+ Descending = 1
+}
diff --git a/components/AdvancedCollectionView/src/VectorChangedEventArgs.cs b/components/AdvancedCollectionView/src/VectorChangedEventArgs.cs
new file mode 100644
index 00000000..93136bd1
--- /dev/null
+++ b/components/AdvancedCollectionView/src/VectorChangedEventArgs.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// Vector changed EventArgs
+///
+internal class VectorChangedEventArgs : IVectorChangedEventArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// collection change type
+ /// index of item changed
+ /// item changed
+ public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!)
+ {
+ CollectionChange = cc;
+ Index = (uint)index;
+ }
+
+ ///
+ /// Gets the type of change that occurred in the vector.
+ ///
+ ///
+ /// The type of change in the vector.
+ ///
+ public CollectionChange CollectionChange { get; }
+
+ ///
+ /// Gets the position where the change occurred in the vector.
+ ///
+ ///
+ /// The zero-based position where the change occurred in the vector, if applicable.
+ ///
+ public uint Index { get; }
+}
diff --git a/components/AdvancedCollectionView/tests/AdvancedCollectionView.Tests.projitems b/components/AdvancedCollectionView/tests/AdvancedCollectionView.Tests.projitems
new file mode 100644
index 00000000..4a0d41fc
--- /dev/null
+++ b/components/AdvancedCollectionView/tests/AdvancedCollectionView.Tests.projitems
@@ -0,0 +1,14 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ 6E4FBFA1-F823-455E-B13F-B72991D64931
+
+
+ AdvancedCollectionViewExperiment.Tests
+
+
+
+
+
\ No newline at end of file
diff --git a/components/AdvancedCollectionView/tests/AdvancedCollectionView.Tests.shproj b/components/AdvancedCollectionView/tests/AdvancedCollectionView.Tests.shproj
new file mode 100644
index 00000000..aa515ca8
--- /dev/null
+++ b/components/AdvancedCollectionView/tests/AdvancedCollectionView.Tests.shproj
@@ -0,0 +1,13 @@
+
+
+
+ 6E4FBFA1-F823-455E-B13F-B72991D64931
+ 14.0
+
+
+
+
+
+
+
+
diff --git a/components/AdvancedCollectionView/tests/Test_AdvancedCollectionView.cs b/components/AdvancedCollectionView/tests/Test_AdvancedCollectionView.cs
new file mode 100644
index 00000000..92131c3e
--- /dev/null
+++ b/components/AdvancedCollectionView/tests/Test_AdvancedCollectionView.cs
@@ -0,0 +1,113 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+namespace AdvancedCollectionViewExperiment.Tests;
+
+[TestClass]
+public class Test_AdvancedCollectionView
+{
+ private class SampleClass : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private int val;
+
+ public int Val
+ {
+ get
+ {
+ return val;
+ }
+
+ set
+ {
+ val = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public int GetPropertyChangedEventHandlerSubscriberLength()
+ {
+ return PropertyChanged is null ? 0 : PropertyChanged.GetInvocationList().Length;
+ }
+
+ public SampleClass(int val)
+ {
+ this.Val = val;
+ }
+
+ private void OnPropertyChanged([CallerMemberName] string name = "")
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+ }
+
+ [TestCategory("Helpers")]
+ [UITestMethod]
+ public void Test_SourceNcc_CollectionChanged_Add()
+ {
+ // Create ref list with all test items:
+ List refList = new List();
+ for (int e = 0; e < 100; e++)
+ {
+ refList.Add(new SampleClass(e));
+ }
+
+ ObservableCollection col = new ObservableCollection();
+ AdvancedCollectionView acv = new AdvancedCollectionView(col, true);
+
+ // Add all items to collection while DeferRefresh() is active:
+ using (acv.DeferRefresh())
+ {
+ foreach (var item in refList)
+ {
+ col.Add(item);
+ }
+ }
+
+ // Check if subscribed to all items:
+ foreach (var item in refList)
+ {
+ Assert.IsTrue(item.GetPropertyChangedEventHandlerSubscriberLength() == 1);
+ }
+ }
+
+ [TestCategory("Helpers")]
+ [UITestMethod]
+ public void Test_SourceNcc_CollectionChanged_Remove()
+ {
+ // Create ref list with all test items:
+ List refList = new List();
+ for (int e = 0; e < 100; e++)
+ {
+ refList.Add(new SampleClass(e));
+ }
+
+ ObservableCollection col = new ObservableCollection();
+ AdvancedCollectionView acv = new AdvancedCollectionView(col, true);
+
+ // Add all items to collection:
+ foreach (var item in refList)
+ {
+ col.Add(item);
+ }
+
+ // Remove all items from collection while DeferRefresh() is active:
+ using (acv.DeferRefresh())
+ {
+ while (col.Count > 0)
+ {
+ col.RemoveAt(0);
+ }
+ }
+
+ // Check if unsubscribed from all items:
+ foreach (var item in refList)
+ {
+ Assert.IsTrue(item.GetPropertyChangedEventHandlerSubscriberLength() == 0);
+ }
+ }
+}
diff --git a/components/CameraPreview/samples/CameraPreviewSample.xaml.cs b/components/CameraPreview/samples/CameraPreviewSample.xaml.cs
index 0936f765..d2750aaa 100644
--- a/components/CameraPreview/samples/CameraPreviewSample.xaml.cs
+++ b/components/CameraPreview/samples/CameraPreviewSample.xaml.cs
@@ -17,6 +17,7 @@ namespace CameraPreviewExperiment.Samples;
[ToolkitSampleBoolOption("ShowCamera", true, Title = "Show camera toggle button")]
[ToolkitSample(id: nameof(CameraPreviewSample), "CameraPreview", description: $"A sample for showing how to create and use a {nameof(CameraPreview)} control.")]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Controls dispose resources when unloaded")]
public sealed partial class CameraPreviewSample : Page
{
private static SemaphoreSlim? semaphoreSlim;
@@ -28,13 +29,24 @@ public CameraPreviewSample()
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
this.InitializeComponent();
- this.Loaded += this.CameraPreviewSample_Loaded;
+
+ Loaded += this.CameraPreviewSample_Loaded;
+ Unloaded += this.CameraPreviewSample_Unloaded;
+
semaphoreSlim = new SemaphoreSlim(1);
-
+ }
+
+ private void CameraPreviewSample_Unloaded(object sender, RoutedEventArgs e)
+ {
+ Unloaded -= this.CameraPreviewSample_Unloaded;
+
+ _softwareBitmapSource?.Dispose();
}
private void CameraPreviewSample_Loaded(object sender, RoutedEventArgs e)
{
+ Loaded -= this.CameraPreviewSample_Loaded;
+
Load();
}
@@ -130,19 +142,19 @@ private async void CaptureButton_Click(object sender, RoutedEventArgs e)
private void UnsubscribeFromEvents()
{
- if (CameraPreviewControl.CameraHelper != null)
- {
- CameraPreviewControl.CameraHelper.FrameArrived -= CameraPreviewControl_FrameArrived!;
- }
+ if (CameraPreviewControl.CameraHelper != null)
+ {
+ CameraPreviewControl.CameraHelper.FrameArrived -= CameraPreviewControl_FrameArrived!;
+ }
- CameraPreviewControl.PreviewFailed -= CameraPreviewControl_PreviewFailed!;
+ CameraPreviewControl.PreviewFailed -= CameraPreviewControl_PreviewFailed!;
}
private async Task CleanUpAsync()
{
UnsubscribeFromEvents();
- CameraPreviewControl.Stop();
- await CameraPreviewControl.CameraHelper.CleanUpAsync();
+ CameraPreviewControl.Stop();
+ await CameraPreviewControl.CameraHelper.CleanUpAsync();
}
}
diff --git a/components/CameraPreview/src/CameraPreview.cs b/components/CameraPreview/src/CameraPreview.cs
index 629a8c4a..77b620bd 100644
--- a/components/CameraPreview/src/CameraPreview.cs
+++ b/components/CameraPreview/src/CameraPreview.cs
@@ -13,6 +13,7 @@ namespace CommunityToolkit.WinUI.Controls;
///
[TemplatePart(Name = Preview_MediaPlayerElementControl, Type = typeof(MediaPlayerElement))]
[TemplatePart(Name = Preview_FrameSourceGroupButton, Type = typeof(Button))]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Implemented via Stop()")]
public partial class CameraPreview : Control
{
private CameraHelper _cameraHelper;
diff --git a/components/Helpers/samples/CameraHelperSample.xaml.cs b/components/Helpers/samples/CameraHelperSample.xaml.cs
index 3acdb45c..f4ece300 100644
--- a/components/Helpers/samples/CameraHelperSample.xaml.cs
+++ b/components/Helpers/samples/CameraHelperSample.xaml.cs
@@ -16,6 +16,7 @@
namespace HelpersExperiment.Samples;
[ToolkitSample(id: nameof(CameraHelperSample), "CameraHelper", description: $"A sample for showing how to use {nameof(CameraHelper)}.")]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Controls dispose resources on unload")]
public sealed partial class CameraHelperSample : UserControl
{
private CameraHelper _cameraHelper;
@@ -157,5 +158,7 @@ private async Task CleanUpAsync()
await _cameraHelper.CleanUpAsync();
_cameraHelper = null!;
}
+
+ _softwareBitmapSource?.Dispose();
}
}
diff --git a/components/Helpers/src/CameraHelper/CameraHelper.cs b/components/Helpers/src/CameraHelper/CameraHelper.cs
index 45622e9a..b0e1a33b 100644
--- a/components/Helpers/src/CameraHelper/CameraHelper.cs
+++ b/components/Helpers/src/CameraHelper/CameraHelper.cs
@@ -15,7 +15,6 @@ namespace CommunityToolkit.WinUI.Helpers;
///
#pragma warning disable CA1063 // Implement IDisposable Correctly
public class CameraHelper : IDisposable
-#pragma warning restore CA1063 // Implement IDisposable Correctly
{
private static IReadOnlyList? _frameSourceGroups;
#pragma warning disable CA2213 // Disposable fields should be disposed
@@ -310,3 +309,4 @@ public async void Dispose()
}
}
}
+#pragma warning restore CA1063 // Implement IDisposable Correctly
diff --git a/tooling b/tooling
index 6bc1f28e..f5ca2c47 160000
--- a/tooling
+++ b/tooling
@@ -1 +1 @@
-Subproject commit 6bc1f28e5e3dc5af9432316e700a761fef14a8d2
+Subproject commit f5ca2c47036a84d2c3d5983c8b17008daac39a45