Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
/p:Version=0.0.${{ github.run_number }}-dev

- name: Setup VSTest
uses: darenm/Setup-VSTest@v1
uses: darenm/Setup-VSTest@v1.3

- name: Build Tests
run: |
Expand Down
3 changes: 2 additions & 1 deletion src/ColumnFilterHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
Expand Down Expand Up @@ -40,7 +41,7 @@ public virtual IList<TableViewFilterItem> GetFilterItems(TableViewColumn column,
}));
}

collectionView.Source = column.TableView.ItemsSource;
collectionView.Source = (column.TableView.ItemsSource as IEnumerable) ?? Enumerable.Empty<object>();

var items = _tableView.ShowFilterItemsCount ?
GetFilterItemsWithCount(column, searchText, collectionView) :
Expand Down
83 changes: 82 additions & 1 deletion src/Extensions/CollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

Expand Down Expand Up @@ -38,4 +40,83 @@ public static void RemoveWhere<T>(this ICollection<T> collection, Predicate<T> p
collection.Remove(item);
}
}

/// <summary>
/// Gets the index of the first occurrence of an item in the enumerable.
/// </summary>
/// <param name="enumerable">The enumerable to search.</param>
/// <param name="item">The item to find.</param>
/// <returns>The index of the item, or -1 if not found.</returns>
public static int IndexOf(this IEnumerable enumerable, object? item)
{
if (enumerable is IList list)
return list.IndexOf(item);

if (enumerable is ICollectionView collectionView)
return collectionView.IndexOf(item);


var index = 0;
foreach (var element in enumerable)
{
if (Equals(element, item))
return index;

index++;
}

return -1;
}

/// <summary>
/// Gets a value indicating whether the enumerable is read-only.
/// </summary>
/// <param name="enumerable">The enumerable to check.</param>
/// <returns></returns>
public static bool IsReadOnly(this IEnumerable enumerable)
{
if (enumerable is IList list)
return list.IsReadOnly;

if (enumerable is ICollectionView collectionView)
return collectionView.IsReadOnly;

return true;
}

public static void Add(this IEnumerable enumerable, object? item)
{
if (enumerable is IList list)
list.Add(item);

if (enumerable is ICollectionView collectionView)
collectionView.Add(item);
}

public static void Insert(this IEnumerable enumerable, int index, object? item)
{
if (enumerable is IList list)
list.Insert(index, item);

if (enumerable is ICollectionView collectionView)
collectionView.Insert(index, item);
}

public static void Remove(this IEnumerable enumerable, object? item)
{
if (enumerable is IList list)
list.Remove(item);

if (enumerable is ICollectionView collectionView)
collectionView.Remove(item);
}

public static void Clear(this IEnumerable enumerable)
{
if (enumerable is IList list)
list.Clear();

if (enumerable is ICollectionView collectionView)
collectionView.Clear();
}
}
6 changes: 5 additions & 1 deletion src/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
// Simple property access
else
{
var propertyInfo = current.Type.GetProperty(part, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)

Check warning on line 91 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties', 'DynamicallyAccessedMemberTypes.NonPublicProperties' in call to 'System.Type.GetProperty(String, BindingFlags)'. The return value of method 'System.Linq.Expressions.Expression.Type.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Check warning on line 91 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties', 'DynamicallyAccessedMemberTypes.NonPublicProperties' in call to 'System.Type.GetProperty(String, BindingFlags)'. The return value of method 'System.Linq.Expressions.Expression.Type.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
?? throw new ArgumentException($"Property '{part}' not found on type '{current.Type.Name}'");

nextPropertyAccess = Expression.Property(current, propertyInfo);
Expand Down Expand Up @@ -253,7 +253,7 @@
return false;

// Find IDictionary<TKey, TValue> interface
var dictionaryInterface = type.GetInterfaces()

Check warning on line 256 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'WinUI.TableView.Extensions.ObjectExtensions.TryCreateDictionaryTryGetExpression(Type, ParameterExpression, Object[], out Expression)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Check warning on line 256 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'WinUI.TableView.Extensions.ObjectExtensions.TryCreateDictionaryTryGetExpression(Type, ParameterExpression, Object[], out Expression)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>));

if (dictionaryInterface == null)
Expand All @@ -272,7 +272,7 @@
}

// Get TryGetValue method
var tryGetValueMethod = dictionaryInterface.GetMethod("TryGetValue");

Check warning on line 275 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethod(String)'. The return value of method 'System.Linq.Enumerable.FirstOrDefault<TSource>(IEnumerable<TSource>, Func<TSource, Boolean>)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Check warning on line 275 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethod(String)'. The return value of method 'System.Linq.Enumerable.FirstOrDefault<TSource>(IEnumerable<TSource>, Func<TSource, Boolean>)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
if (tryGetValueMethod == null)
throw new InvalidOperationException($"The dictionary type {type} has no TryGetValue method"); // should not happen

Expand Down Expand Up @@ -307,12 +307,12 @@
return false;

// Try to find an indexer property with the appropriate parameter types
var indexerProperty = type.GetProperty("Item", [typeof(int)]);

Check warning on line 310 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String, Type[])'. The parameter 'type' of method 'WinUI.TableView.Extensions.ObjectExtensions.TryCreateIListOrICollectionBoundsCheckExpression(Type, ParameterExpression, Object[], out Expression)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Check warning on line 310 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String, Type[])'. The parameter 'type' of method 'WinUI.TableView.Extensions.ObjectExtensions.TryCreateIListOrICollectionBoundsCheckExpression(Type, ParameterExpression, Object[], out Expression)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
if (indexerProperty == null)
return false; // No indexer found

// Check for IList<T>
var listInterface = type.GetInterfaces()

Check warning on line 315 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'WinUI.TableView.Extensions.ObjectExtensions.TryCreateIListOrICollectionBoundsCheckExpression(Type, ParameterExpression, Object[], out Expression)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Check warning on line 315 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'WinUI.TableView.Extensions.ObjectExtensions.TryCreateIListOrICollectionBoundsCheckExpression(Type, ParameterExpression, Object[], out Expression)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
.FirstOrDefault(i => i.IsGenericType &&
(i.GetGenericTypeDefinition() == typeof(IList<>) ||
i.GetGenericTypeDefinition() == typeof(ICollection<>)
Expand All @@ -326,7 +326,7 @@
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(indices), $"Index for (generic) IList/ICollection cannot be negative: {index}");

var countProperty = type.GetProperty("Count");

Check warning on line 329 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The parameter 'type' of method 'WinUI.TableView.Extensions.ObjectExtensions.TryCreateIListOrICollectionBoundsCheckExpression(Type, ParameterExpression, Object[], out Expression)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

if (indexerProperty != null && countProperty != null)
{
Expand Down Expand Up @@ -357,7 +357,7 @@

// Try to find an indexer property with the appropriate parameter types
var indexerTypes = indices.Select(idx => idx.GetType()).ToArray();
var indexerProperty = type.GetProperty("Item", indexerTypes);

Check warning on line 360 in src/Extensions/ObjectExtensions.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String, Type[])'. The parameter 'type' of method 'WinUI.TableView.Extensions.ObjectExtensions.TryCreateGenericIndexerExpression(Type, ParameterExpression, Object[], out Expression)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

if (indexerProperty == null)
return false;
Expand Down Expand Up @@ -400,6 +400,7 @@
var listType = list.GetType();
Type? itemType = null;
var isICustomTypeProvider = false;
var isWinRTObject = false;

// If it's a generic enumerable, get the generic type.

Expand All @@ -410,11 +411,14 @@
if (itemType != null)
{
isICustomTypeProvider = typeof(ICustomTypeProvider).IsAssignableFrom(itemType);
#if WINDOWS
isWinRTObject = typeof(WinRT.IInspectable).IsAssignableFrom(itemType);
#endif
}

// Bare IEnumerables mean that result type will be object. In that case, try to get something more interesting.
// Or, if the itemType implements ICustomTypeProvider, try to retrieve the custom type from one of the object instances.
if (itemType == null || itemType == typeof(object) || isICustomTypeProvider)
if (itemType == null || itemType == typeof(object) || isICustomTypeProvider || isWinRTObject)
{
// No type was located yet. Does the list have anything in it?
Type? firstItemType = null;
Expand Down
18 changes: 8 additions & 10 deletions src/ItemsSource/CollectionView.Properties.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Microsoft.UI.Xaml.Data;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Windows.Foundation.Collections;
using WinUI.TableView.Extensions;

namespace WinUI.TableView;

Expand All @@ -11,24 +11,22 @@ partial class CollectionView
/// <summary>
/// Gets or sets the source collection.
/// </summary>
public IList Source
public IEnumerable Source
{
get => _source;
set
{
if (_source == value) return;

if (_source is not null) DetachPropertyChangedHandlers(_source);
DetachCollectionChangedHandlers(_source);
DetachPropertyChangedHandlers(_source);

_source = value;
AttachPropertyChangedHandlers(_source);

_collectionChangedListener?.Detach();
AttachCollectionChangedHandlers(_source);
AttachPropertyChangedHandlers(_source);

if (_source is INotifyCollectionChanged sourceNcc)
{
_collectionChangedListener = new(this, sourceNcc, OnSourceCollectionChanged);
}
CreateItemsCopy(_source);

HandleSourceChanged();
OnPropertyChanged();
Expand Down Expand Up @@ -103,7 +101,7 @@ public object? CurrentItem
/// <summary>
/// Gets a value indicating whether the collection is read-only.
/// </summary>
public bool IsReadOnly => _source == null || _source.IsReadOnly;
public bool IsReadOnly => _source == null || _source.IsReadOnly();

/// <summary>
/// Gets or sets a value indicating whether live shaping is enabled.
Expand Down
120 changes: 112 additions & 8 deletions src/ItemsSource/CollectionView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using System.Linq;
using Windows.Foundation;
using Windows.Foundation.Collections;
using WinUI.TableView.Helpers;
using WinUI.TableView.Extensions;

namespace WinUI.TableView;

Expand All @@ -17,19 +17,19 @@ namespace WinUI.TableView;
/// </summary>
internal partial class CollectionView : ICollectionView, ISupportIncrementalLoading, INotifyPropertyChanged, IComparer<object?>
{
private IList _source = default!;
private IEnumerable _source = new List<object>();
private object[] _itemsCopy = []; // In case the source is ICollection, keep a copy of the items to keep track of removed items.
private bool _allowLiveShaping;
private readonly List<object?> _view = [];
private readonly ObservableCollection<FilterDescription> _filterDescriptions = [];
private readonly ObservableCollection<SortDescription> _sortDescriptions = [];
private CollectionChangedListener<CollectionView>? _collectionChangedListener;

/// <summary>
/// Initializes a new instance of the <see cref="CollectionView"/> class.
/// </summary>
/// <param name="source">The source collection.</param>
/// <param name="liveShapingEnabled">Indicates whether live shaping is enabled.</param>
public CollectionView(IList? source = null, bool liveShapingEnabled = true)
public CollectionView(IEnumerable? source = null, bool liveShapingEnabled = true)
{
_filterDescriptions.CollectionChanged += OnFilterDescriptionsCollectionChanged;
_sortDescriptions.CollectionChanged += OnSortDescriptionsCollectionChanged;
Expand Down Expand Up @@ -64,6 +64,36 @@ private void OnSortDescriptionsCollectionChanged(object? sender, NotifyCollectio
HandleSortChanged();
}

/// <summary>
/// Attaches collection changed handlers to the source collection.
/// </summary>
private void AttachCollectionChangedHandlers(IEnumerable source)
{
if (source is INotifyCollectionChanged sourceNcc)
{
sourceNcc.CollectionChanged += OnSourceCollectionChanged;
}
else if (source is ICollectionView sourceCV)
{
sourceCV.VectorChanged += OnSourceVectorChanged;
}
}

/// <summary>
/// Detaches collection changed handlers from the source collection.
/// </summary>
private void DetachCollectionChangedHandlers(IEnumerable source)
{
if (source is INotifyCollectionChanged sourceNcc)
{
sourceNcc.CollectionChanged -= OnSourceCollectionChanged;
}
else if (source is ICollectionView sourceCV)
{
sourceCV.VectorChanged -= OnSourceVectorChanged;
}
}

/// <summary>
/// Attaches property changed handlers to the items in the collection.
/// </summary>
Expand Down Expand Up @@ -92,6 +122,75 @@ private void DetachPropertyChangedHandlers(IEnumerable? items)
}
}

/// <summary>
/// Handles changes to the source vector.
/// </summary>
private void OnSourceVectorChanged(IObservableVector<object> sender, IVectorChangedEventArgs args)
{
var index = (int)args.Index;

switch (args.CollectionChange)
{
case CollectionChange.ItemInserted:
if (_deferCounter <= 0)
{
if (index < Count)
{
var item = sender[index];
AttachPropertyChangedHandlers(new object[] { item });
HandleItemAdded(index, item);
}
else
{
HandleSourceChanged();
}
}

break;
case CollectionChange.ItemRemoved:
if (_deferCounter <= 0)
{
if (index < _itemsCopy.Length)
{
var item = _itemsCopy[index];
DetachPropertyChangedHandlers(new object[] { item });
HandleItemRemoved(index, item);
}
else
{
HandleSourceChanged();
}
}

break;
case CollectionChange.ItemChanged:
case CollectionChange.Reset:
if (_deferCounter <= 0)
{
HandleSourceChanged();
}

DetachPropertyChangedHandlers(_itemsCopy);
AttachPropertyChangedHandlers(_source);

break;
}

CreateItemsCopy(_source);
}

/// <summary>
/// Creates a copy of the items from the collection if it implements ICollectionView.
/// </summary>
private void CreateItemsCopy(IEnumerable source)
{
if (source is ICollectionView collectionView)
{
_itemsCopy = new object[collectionView.Count];
collectionView.CopyTo(_itemsCopy, 0);
}
}

/// <summary>
/// Handles changes to the source collection.
/// </summary>
Expand Down Expand Up @@ -137,6 +236,9 @@ private void OnSourceCollectionChanged(object? arg1, NotifyCollectionChangedEven
HandleSourceChanged();
}

DetachPropertyChangedHandlers(e.OldItems);
AttachPropertyChangedHandlers(_source);

break;
}
}
Expand Down Expand Up @@ -242,7 +344,7 @@ private void HandleSourceChanged()
/// </summary>
private void HandleFilterChanged()
{
if (FilterDescriptions.Any())
if (FilterDescriptions.Count > 0)
{
for (var index = 0; index < _view.Count; index++)
{
Expand All @@ -259,19 +361,21 @@ private void HandleFilterChanged()

var viewHash = new HashSet<object?>(_view);
var viewIndex = 0;
for (var index = 0; index < _source.Count; index++)
var i = 0;
foreach (var item in _source)
{
var item = _source[index]!;
if (viewHash.Contains(item))
{
viewIndex++;
continue;
}

if (HandleItemAdded(index, item, viewIndex))
if (HandleItemAdded(i, item, viewIndex))
{
viewIndex++;
}

i++;
}
}

Expand Down
Loading
Loading