-
-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Handle ItemsSource updates without an ObservableCollection (#138)
- Loading branch information
1 parent
ba00b02
commit 0103dd2
Showing
16 changed files
with
575 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
src/BlazorBindings.Maui/Elements/Internal/ItemsSourceComponent.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
using System.Collections.ObjectModel; | ||
using System.Diagnostics; | ||
|
||
namespace BlazorBindings.Maui.Elements.Internal; | ||
|
||
/// <summary> | ||
/// This component creates an observable collection, which is updated by blazor renderer. | ||
/// This allows to use it for cases, when MAUI expects an ObservableCollection to handle the updates, | ||
/// but instead of forcing the user to use ObservableCollection on their side, we manage the updates by Blazor. | ||
/// Probably not the most performant way, is there any other option? | ||
/// </summary> | ||
internal class ItemsSourceComponent<TControl, TItem> : NativeControlComponentBase, IElementHandler, IContainerElementHandler, INonPhysicalChild | ||
{ | ||
private readonly ObservableCollection<TItem> _observableCollection = new(); | ||
|
||
[Parameter] | ||
public IEnumerable<TItem> Items { get; set; } | ||
|
||
[Parameter] | ||
public Action<TControl, ObservableCollection<TItem>> CollectionSetter { get; set; } | ||
|
||
[Parameter] | ||
public Func<TItem, object> KeySelector { get; set; } | ||
|
||
|
||
private TControl _parent; | ||
public object TargetElement => _parent; | ||
|
||
private HashSet<object> _keys; | ||
|
||
protected override RenderFragment GetChildContent() => builder => | ||
{ | ||
_keys?.Clear(); | ||
bool shouldAddKey = true; | ||
int index = 0; | ||
foreach (var item in Items) | ||
{ | ||
var key = KeySelector == null ? item : KeySelector(item); | ||
if (KeySelector == null) | ||
{ | ||
// Blazor doesn't allow duplicate keys. Therefore we add keys until the first duplicate. | ||
// In case KeySelector is provided, we don't check for that here, since it's user's responsibility now. | ||
_keys ??= new(); | ||
shouldAddKey &= _keys.Add(key); | ||
if (!shouldAddKey) | ||
key = null; | ||
} | ||
builder.OpenComponent<ItemHolderComponent>(1); | ||
builder.SetKey(key); | ||
builder.AddAttribute(2, nameof(ItemHolderComponent.Item), item); | ||
builder.AddAttribute(3, nameof(ItemHolderComponent.Index), index); | ||
builder.AddAttribute(4, nameof(ItemHolderComponent.ObservableCollection), _observableCollection); | ||
if (key != null) | ||
builder.AddAttribute(5, nameof(ItemHolderComponent.HasKey), true); | ||
builder.CloseComponent(); | ||
index++; | ||
} | ||
}; | ||
|
||
void IContainerElementHandler.AddChild(object child, int physicalSiblingIndex) | ||
{ | ||
_observableCollection.Insert(physicalSiblingIndex, (TItem)child); | ||
} | ||
|
||
void IContainerElementHandler.RemoveChild(object child, int physicalSiblingIndex) | ||
{ | ||
Debug.Assert(Equals(_observableCollection[physicalSiblingIndex], child)); | ||
_observableCollection.RemoveAt(physicalSiblingIndex); | ||
} | ||
|
||
void IContainerElementHandler.ReplaceChild(int physicalSiblingIndex, object oldChild, object newChild) | ||
{ | ||
Debug.Assert(Equals(_observableCollection[physicalSiblingIndex], oldChild)); | ||
if (!Equals(_observableCollection[physicalSiblingIndex], newChild)) | ||
_observableCollection[physicalSiblingIndex] = (TItem)newChild; | ||
} | ||
|
||
public void RemoveFromParent(object parentElement) | ||
{ | ||
} | ||
|
||
public void SetParent(object parentElement) | ||
{ | ||
_parent = (TControl)parentElement; | ||
CollectionSetter(_parent, _observableCollection); | ||
} | ||
|
||
private class ItemHolderComponent : NativeControlComponentBase, IElementHandler | ||
{ | ||
[Parameter] | ||
public TItem Item { get; set; } | ||
|
||
[Parameter] | ||
public ObservableCollection<TItem> ObservableCollection { get; set; } | ||
|
||
[Parameter] | ||
public int? Index { get; set; } | ||
|
||
[Parameter] | ||
public bool HasKey { get; set; } | ||
|
||
public object TargetElement => Item; | ||
|
||
public override Task SetParametersAsync(ParameterView parameters) | ||
{ | ||
var previousIndex = Index; | ||
var previousItem = Item; | ||
|
||
// Task should be completed immediately | ||
var task = base.SetParametersAsync(parameters); | ||
|
||
if (previousIndex == null) | ||
return task; | ||
|
||
if (previousIndex == Index && Equals(previousItem, Item)) | ||
return task; | ||
|
||
// Generally it will not be invoked, but it is needed when Source has duplicate items, or component has key. | ||
// The problem here is that we don't know whether previous items are going to be removed or added. | ||
// We use previousIndex here, because this part of the code is executed before items are actually added/removed to ObservableCollection. | ||
if ((HasKey || previousIndex == Index) && !Equals(ObservableCollection[previousIndex.Value], Item)) | ||
ObservableCollection[previousIndex.Value] = Item; | ||
|
||
return task; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
using Microsoft.AspNetCore.Components.Rendering; | ||
using System.Collections.Immutable; | ||
using System.Collections.Specialized; | ||
using MC = Microsoft.Maui.Controls; | ||
|
||
namespace BlazorBindings.Maui.Elements; | ||
|
||
public abstract partial class ItemsView<T> | ||
{ | ||
[Parameter] public IEnumerable<T> ItemsSource { get; set; } | ||
[Parameter] public Func<T, object> ItemKeySelector { get; set; } | ||
[Parameter] public RenderFragment<T> ItemTemplateSelector { get; set; } | ||
|
||
// Whether we should attempt to create ObservableCollection on our own (via diffing), or assign it directly. | ||
private bool AssignItemsSourceDirectly => ItemsSource is INotifyCollectionChanged || ItemsSource is IImmutableList<T>; | ||
|
||
protected override bool HandleAdditionalParameter(string name, object value) | ||
{ | ||
switch (name) | ||
{ | ||
case nameof(ItemsSource): | ||
if (!Equals(ItemsSource, value)) | ||
{ | ||
ItemsSource = (IEnumerable<T>)value; | ||
|
||
if (AssignItemsSourceDirectly) | ||
NativeControl.ItemsSource = ItemsSource; | ||
} | ||
return true; | ||
|
||
case nameof(ItemKeySelector): | ||
ItemKeySelector = (Func<T, object>)value; | ||
return true; | ||
|
||
case nameof(ItemTemplateSelector): | ||
ItemTemplateSelector = (RenderFragment<T>)value; | ||
return true; | ||
} | ||
|
||
return base.HandleAdditionalParameter(name, value); | ||
} | ||
|
||
protected override void RenderAdditionalPartialElementContent(RenderTreeBuilder builder, ref int sequence) | ||
{ | ||
base.RenderAdditionalPartialElementContent(builder, ref sequence); | ||
|
||
RenderTreeBuilderHelper.AddDataTemplateSelectorProperty<MC.ItemsView, T>(builder, sequence++, ItemTemplateSelector, (x, template) => x.ItemTemplate = template); | ||
|
||
sequence++; | ||
if (!AssignItemsSourceDirectly) | ||
RenderTreeBuilderHelper.AddItemsSourceProperty<MC.ItemsView, T>(builder, sequence, ItemsSource, ItemKeySelector, (x, items) => x.ItemsSource = items); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
src/BlazorBindings.UnitTests/Elements/CollectionViewTests.razor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
@using System.Collections.Specialized; | ||
@using System.Collections.ObjectModel; | ||
@using System.Collections.Immutable; | ||
|
||
@inherits ElementTestBase | ||
|
||
@code { | ||
[Test] | ||
public async Task CreateCollectionViewWithItemTemplate() | ||
{ | ||
var items = new (int Index, string Name)[] { (1, "First"), (2, "Seconds"), (3, ("Third")) }; | ||
var collectionView = await Render<MC.CollectionView>( | ||
@<CollectionView ItemsSource="items"> | ||
<ItemTemplate> | ||
<VerticalStackLayout> | ||
<Label>@context.Index</Label> | ||
<Label>@context.Name</Label> | ||
</VerticalStackLayout> | ||
</ItemTemplate> | ||
</CollectionView>); | ||
|
||
// It's nice that it doesn't crash at least, but is there any way to get templated items here?... | ||
Assert.That(collectionView.ItemsSource, Is.EqualTo(items)); | ||
|
||
// For a regular collections we attempt to detect changes in the collection. | ||
Assert.That(collectionView.ItemsSource is INotifyCollectionChanged); | ||
} | ||
|
||
[Test] | ||
public async Task ObservableItemsViewIsAssignedDirectly() | ||
{ | ||
var items = new ObservableCollection<int>(new[] { 1, 2, 3, 4 }); | ||
|
||
var collectionView = await Render<MC.CollectionView>( | ||
@<CollectionView ItemsSource="items"> | ||
<ItemTemplate> | ||
<Label>@context</Label> | ||
</ItemTemplate> | ||
</CollectionView>); | ||
|
||
// Since collection is already observable, no need to diff additionally. | ||
Assert.That(collectionView.ItemsSource, Is.SameAs(items)); | ||
} | ||
|
||
[Test] | ||
public async Task ImmutableItemsViewIsAssignedDirectly() | ||
{ | ||
var items = ImmutableList.Create(1, 2, 3, 4); | ||
|
||
var collectionView = await Render<MC.CollectionView>( | ||
@<CollectionView ItemsSource="items"> | ||
<ItemTemplate> | ||
<Label>@context</Label> | ||
</ItemTemplate> | ||
</CollectionView>); | ||
|
||
// Since collection is immutable, there is no point in detecting changes there. | ||
Assert.That(collectionView.ItemsSource, Is.SameAs(items)); | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
Oops, something went wrong.