Skip to content

Commit

Permalink
Support ReplaceChildElement in ElementManager
Browse files Browse the repository at this point in the history
  • Loading branch information
Dreamescaper committed Sep 4, 2023
1 parent 42b983f commit 974ccb9
Show file tree
Hide file tree
Showing 11 changed files with 337 additions and 129 deletions.
11 changes: 11 additions & 0 deletions src/BlazorBindings.Core/ElementManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,15 @@ public virtual void RemoveChildElement(IElementHandler parentHandler, IElementHa
$"(child type is '{childHandler.TargetElement?.GetType().FullName}').");
}
}

public virtual void ReplaceChildElement(IElementHandler parentHandler, IElementHandler oldChild, IElementHandler newChild, int physicalSiblingIndex)
{
if (oldChild is INonPhysicalChild || newChild is INonPhysicalChild)
throw new NotSupportedException("NonPhysicalChild elements cannot be replaced.");

if (parentHandler is not IContainerElementHandler container)
throw new InvalidOperationException($"Handler of type '{parentHandler.GetType().FullName}' does not support replacing child elements.");

container.ReplaceChild(physicalSiblingIndex, oldChild.TargetElement, newChild.TargetElement);
}
}
6 changes: 6 additions & 0 deletions src/BlazorBindings.Core/IContainerElementHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ public interface IContainerElementHandler : IElementHandler
{
void AddChild(object child, int physicalSiblingIndex);
void RemoveChild(object child, int physicalSiblingIndex);

void ReplaceChild(int physicalSiblingIndex, object oldChild, object newChild)
{
RemoveChild(oldChild, physicalSiblingIndex);
AddChild(newChild, physicalSiblingIndex);
}
}
220 changes: 146 additions & 74 deletions src/BlazorBindings.Core/NativeComponentAdapter.cs

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions src/BlazorBindings.Core/NativeComponentRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,23 @@ public void RemoveRootComponent(IComponent component)

protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
HashSet<int> processedComponentIds = new HashSet<int>();
HashSet<NativeComponentAdapter> adaptersWithPendingEdits = new();

var numUpdatedComponents = renderBatch.UpdatedComponents.Count;
for (var componentIndex = 0; componentIndex < numUpdatedComponents; componentIndex++)
{
var updatedComponent = renderBatch.UpdatedComponents.Array[componentIndex];

// If UpdatedComponent is already processed (due to recursive ApplyEdits) - skip it.
if (updatedComponent.Edits.Count > 0 && !processedComponentIds.Contains(updatedComponent.ComponentId))
if (updatedComponent.Edits.Count > 0)
{
var adapter = _componentIdToAdapter[updatedComponent.ComponentId];
adapter.ApplyEdits(updatedComponent.ComponentId, updatedComponent.Edits, renderBatch.ReferenceFrames, renderBatch, processedComponentIds);
adapter.ApplyEdits(updatedComponent.ComponentId, updatedComponent.Edits, renderBatch, adaptersWithPendingEdits);
}
}

foreach (var adapter in adaptersWithPendingEdits.OrderByDescending(a => a.DeepLevel))
adapter.ApplyPendingEdits();

var numDisposedComponents = renderBatch.DisposedComponentIDs.Count;
for (var i = 0; i < numDisposedComponents; i++)
{
Expand Down
9 changes: 3 additions & 6 deletions src/BlazorBindings.Maui/Elements/FlyoutPage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components.Rendering;
using BlazorBindings.Maui.Extensions;
using Microsoft.AspNetCore.Components.Rendering;
using MC = Microsoft.Maui.Controls;

namespace BlazorBindings.Maui.Elements;
Expand All @@ -24,12 +25,8 @@ protected override void RenderAdditionalPartialElementContent(RenderTreeBuilder

RenderTreeBuilderHelper.AddContentProperty<MC.FlyoutPage>(builder, sequence++, Detail, (page, value) =>
{
// We cannot set Detail to null. An actual page will probably be set on next invocation anyway.
if (value == null)
return;
if (value is not MC.NavigationPage navigationPage)
navigationPage = new MC.NavigationPage((MC.Page)value);
navigationPage = new MC.NavigationPage(value.Cast<MC.Page>());
page.Detail = navigationPage;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ void IContainerElementHandler.RemoveChild(object child, int physicalSiblingIndex
SetPropertyAction(_parent, null);
}

void IContainerElementHandler.ReplaceChild(int physicalSiblingIndex, object oldChild, object newChild)
{
SetPropertyAction(_parent, newChild);
}

// Because this is a 'fake' element, all matters related to physical trees
// should be no-ops.
object IElementHandler.TargetElement => null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using BlazorBindings.Maui.Extensions;
using System.Diagnostics;

namespace BlazorBindings.Maui.Elements.Internal;

Expand Down Expand Up @@ -33,6 +34,13 @@ void IContainerElementHandler.AddChild(object child, int physicalSiblingIndex)

void IContainerElementHandler.RemoveChild(object child, int physicalSiblingIndex)
{
Debug.Assert(_propertyItems[physicalSiblingIndex] == child);
_propertyItems.Remove(child.Cast<TItem>());
}

void IContainerElementHandler.ReplaceChild(int physicalSiblingIndex, object oldChild, object newChild)
{
Debug.Assert(_propertyItems[physicalSiblingIndex] == oldChild);
_propertyItems[physicalSiblingIndex] = newChild.Cast<TItem>();
}
}
13 changes: 9 additions & 4 deletions src/BlazorBindings.Maui/Elements/ShellContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

using Microsoft.AspNetCore.Components.Rendering;
using System.Diagnostics;
using MC = Microsoft.Maui.Controls;

namespace BlazorBindings.Maui.Elements;
Expand Down Expand Up @@ -42,10 +43,14 @@ void IContainerElementHandler.AddChild(object child, int physicalSiblingIndex)

void IContainerElementHandler.RemoveChild(object child, int physicalSiblingIndex)
{
if (NativeControl.Content == child)
{
NativeControl.Content = null;
}
Debug.Assert(NativeControl.Content == child);
NativeControl.Content = null;
}

void IContainerElementHandler.ReplaceChild(int physicalSiblingIndex, object oldChild, object newChild)
{
Debug.Assert(NativeControl.Content == oldChild);
NativeControl.Content = newChild;
}

protected override void RenderAdditionalPartialElementContent(RenderTreeBuilder builder, ref int sequence)
Expand Down
64 changes: 64 additions & 0 deletions src/BlazorBindings.UnitTests/Components/TestContainerComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using BlazorBindings.Core;
using BlazorBindings.Maui.Extensions;
using System.Collections.Immutable;

namespace BlazorBindings.UnitTests.Components;

public class TestContainerComponent : NativeControlComponentBase, IElementHandler, IContainerElementHandler
{
private TestTargetElement _targetElement = new();

[Parameter] public int X { get; set; }
[Parameter] public int Y { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }

protected override RenderFragment GetChildContent() => ChildContent;

public override async Task SetParametersAsync(ParameterView parameters)
{
await base.SetParametersAsync(parameters);
_targetElement.X = X;
_targetElement.Y = Y;
}

public object TargetElement => _targetElement;

void IContainerElementHandler.AddChild(object child, int physicalSiblingIndex)
{
_targetElement.Children = _targetElement.Children.Insert(physicalSiblingIndex, child.Cast<TestTargetElement>());
}

void IContainerElementHandler.RemoveChild(object child, int physicalSiblingIndex)
{
if (!Equals(_targetElement.Children[physicalSiblingIndex], child))
throw new InvalidOperationException("Unexpected child to remove.");

_targetElement.Children = _targetElement.Children.RemoveAt(physicalSiblingIndex);
}

void IContainerElementHandler.ReplaceChild(int physicalSiblingIndex, object oldChild, object newChild)
{
if (!Equals(_targetElement.Children[physicalSiblingIndex], oldChild))
throw new InvalidOperationException("Unexpected child to remove.");

_targetElement.Children = _targetElement.Children.SetItem(physicalSiblingIndex, newChild.Cast<TestTargetElement>());
}

// We want to track element state when it is added to parent, therefore mutable struct is used.
public record struct TestTargetElement
{
public TestTargetElement() : this(0, 0) { }

public TestTargetElement(int x, int y)
{
X = x;
Y = y;
Children = ImmutableList.Create<TestTargetElement>();
}

public int X { get; set; }
public int Y { get; set; }

public ImmutableList<TestTargetElement> Children { get; set; }
}
}
82 changes: 41 additions & 41 deletions src/BlazorBindings.UnitTests/Elements/ElementTestBase.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,58 @@
@implements IHandleEvent

@code {
public ElementTestBase()
{
MC.Application.Current = new TestApplication();
}

private TestBlazorBindingsRenderer _renderer = (TestBlazorBindingsRenderer)TestBlazorBindingsRenderer.Create();
private RenderFragmentComponent _renderedComponent;
public ElementTestBase()
{
MC.Application.Current = new TestApplication();
}

protected async Task<T> Render<T>(RenderFragment renderFragment) where T : MC.BindableObject
{
var container = new RootContainerHandler();
_renderedComponent = await _renderer.AddComponent<RenderFragmentComponent>(container, new Dictionary<string, object>
{
["RenderFragment"] = renderFragment
});
private TestBlazorBindingsRenderer _renderer = (TestBlazorBindingsRenderer)TestBlazorBindingsRenderer.Create();
private RenderFragmentComponent _renderedComponent;

return (T)container.Elements[0];
}
protected async Task<T> Render<T>(RenderFragment renderFragment)
{
var container = new RootContainerHandler();
_renderedComponent = await _renderer.AddComponent<RenderFragmentComponent>(container, new Dictionary<string, object>
{
["RenderFragment"] = renderFragment
});

protected new void StateHasChanged()
{
_renderedComponent?.StateHasChanged();
ThrowOnException();
}
return (T)container.Elements[0];
}

protected void ThrowOnException()
{
if (_renderer.Exceptions.Count > 0)
protected new void StateHasChanged()
{
ExceptionDispatchInfo.Throw(_renderer.Exceptions[0]);
_renderedComponent?.StateHasChanged();
ThrowOnException();
}
}

Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
{
var task = (_renderedComponent as IHandleEvent)?.HandleEventAsync(callback, arg);
ThrowOnException();
return task ?? Task.CompletedTask;
}

private class RenderFragmentComponent : ComponentBase
{
[Parameter] public RenderFragment RenderFragment { get; set; }
protected void ThrowOnException()
{
if (_renderer.Exceptions.Count > 0)
{
ExceptionDispatchInfo.Throw(_renderer.Exceptions[0]);
}
}

protected override void BuildRenderTree(RenderTreeBuilder builder)
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
{
RenderFragment(builder);
var task = (_renderedComponent as IHandleEvent)?.HandleEventAsync(callback, arg);
ThrowOnException();
return task ?? Task.CompletedTask;
}

public new void StateHasChanged()
private class RenderFragmentComponent : ComponentBase
{
base.StateHasChanged();
[Parameter] public RenderFragment RenderFragment { get; set; }

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
RenderFragment(builder);
}

public new void StateHasChanged()
{
base.StateHasChanged();
}
}
}
}
38 changes: 38 additions & 0 deletions src/BlazorBindings.UnitTests/NativeComponentRendererTests.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@using BlazorBindings.UnitTests.Components;
@using BlazorBindings.UnitTests.Elements;

@inherits ElementTestBase

@code
{
[Test]
public async Task TargetElementsShouldHavePropertiesSetWhenAddedToParents()
{
var parent = await Render<TestContainerComponent.TestTargetElement>(
@<TestContainerComponent X="1" Y="2">
<TestContainerComponent X="3" Y="4">
<WrapperWithCascadingValue>
<TestContainerComponent X="5" Y="6" />
</WrapperWithCascadingValue>
<TestContainerComponent X="7" Y="8" />
</TestContainerComponent>

<TestContainerComponent X="9" Y="10" />
</TestContainerComponent>
);

Assert.That((parent.X, parent.Y), Is.EqualTo((1, 2)));

var child0 = parent.Children[0];
var child1 = parent.Children[1];

Assert.That((child0.X, child0.Y), Is.EqualTo((3, 4)));
Assert.That((child1.X, child1.Y), Is.EqualTo((9, 10)));

var child00 = child0.Children[0];
var child01 = child0.Children[1];

Assert.That((child00.X, child00.Y), Is.EqualTo((5, 6)));
Assert.That((child01.X, child01.Y), Is.EqualTo((7, 8)));
}
}

0 comments on commit 974ccb9

Please sign in to comment.