CPF/CPF.Razor/Core/NativeComponentAdapter.cs
2024-01-09 16:57:27 +08:00

471 lines
22 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace CPF.Razor
{
/// <summary>
/// Represents a "shadow" item that Blazor uses to map changes into the live native UI tree.
/// </summary>
[DebuggerDisplay("{DebugName}")]
internal sealed class NativeComponentAdapter : IDisposable
{
private static volatile int DebugInstanceCounter;
public NativeComponentAdapter(NativeComponentRenderer renderer, IElementHandler closestPhysicalParent, IElementHandler knownTargetElement = null)
{
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_closestPhysicalParent = closestPhysicalParent;
_targetElement = knownTargetElement;
// Assign unique counter value. This *should* all be done on one thread, but just in case, make it thread-safe.
_debugInstanceCounterValue = Interlocked.Increment(ref DebugInstanceCounter);
}
private readonly int _debugInstanceCounterValue;
private string DebugName => $"[#{_debugInstanceCounterValue}] {Name}";
public NativeComponentAdapter Parent { get; private set; }
public List<NativeComponentAdapter> Children { get; } = new List<NativeComponentAdapter>();
private readonly IElementHandler _closestPhysicalParent;
private IElementHandler _targetElement;
private IComponent _targetComponent;
public NativeComponentRenderer Renderer { get; }
/// <summary>
/// Used for debugging purposes.
/// </summary>
public string Name { get; internal set; }
public override string ToString()
{
return $"{nameof(NativeComponentAdapter)}: Name={Name ?? "<?>"}, Target={_targetElement?.GetType().Name ?? "<None>"}, #Children={Children.Count}";
}
internal void ApplyEdits(int componentId, ArrayBuilderSegment<RenderTreeEdit> edits, ArrayRange<RenderTreeFrame> referenceFrames, RenderBatch batch)
{
Renderer.Dispatcher.AssertAccess();
if (edits.Count == 0)
{
// TODO: Without this check there's a NullRef in ArrayBuilderSegment? Possibly a Blazor bug?
return;
}
foreach (var edit in edits)
{
switch (edit.Type)
{
case RenderTreeEditType.PrependFrame:
ApplyPrependFrame(batch, componentId, edit.SiblingIndex, referenceFrames.Array, edit.ReferenceFrameIndex);
break;
case RenderTreeEditType.RemoveFrame:
ApplyRemoveFrame(edit.SiblingIndex);
break;
case RenderTreeEditType.SetAttribute:
ApplySetAttribute(ref referenceFrames.Array[edit.ReferenceFrameIndex]);
break;
case RenderTreeEditType.RemoveAttribute:
// TODO: See whether siblingIndex is needed here
ApplyRemoveAttribute(edit.RemovedAttributeName);
break;
case RenderTreeEditType.UpdateText:
{
var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
if (_targetElement is IHandleChildContentText handleChildContentText)
{
handleChildContentText.HandleText(edit.SiblingIndex, frame.TextContent);
}
else
{
throw new Exception("Cannot set text content on child that doesn't handle inner text content.");
}
break;
}
case RenderTreeEditType.StepIn:
{
// TODO: Need to implement this. For now it seems safe to ignore.
break;
}
case RenderTreeEditType.StepOut:
{
// TODO: Need to implement this. For now it seems safe to ignore.
break;
}
case RenderTreeEditType.UpdateMarkup:
{
var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
if (_targetElement is IHandleChildContentText handleChildContentText)
{
handleChildContentText.HandleText(edit.SiblingIndex, frame.MarkupContent);
}
else
{
throw new Exception("Cannot set markup content on child that doesn't handle inner text content.");
}
break;
}
case RenderTreeEditType.PermutationListEntry:
throw new NotImplementedException($"Not supported edit type: {edit.Type}");
case RenderTreeEditType.PermutationListEnd:
throw new NotImplementedException($"Not supported edit type: {edit.Type}");
default:
throw new NotImplementedException($"Invalid edit type: {edit.Type}");
}
}
}
private void ApplyRemoveFrame(int siblingIndex)
{
var childToRemove = Children[siblingIndex];
Children.RemoveAt(siblingIndex);
childToRemove.RemoveSelfAndDescendants();
}
private void RemoveSelfAndDescendants()
{
if (_targetElement != null)
{
// This adapter represents a physical element, so by removing it, we implicitly
// remove all descendants.
Renderer.ElementManager.RemoveElement(_targetElement);
}
else
{
// This adapter is just a container for other adapters
foreach (var child in Children)
{
child.RemoveSelfAndDescendants();
}
}
}
private void ApplySetAttribute(ref RenderTreeFrame attributeFrame)
{
if (_targetElement == null)
{
throw new InvalidOperationException($"Trying to apply attribute {attributeFrame.AttributeName} to an adapter that isn't for an element");
}
_targetElement.ApplyAttribute(
attributeFrame.AttributeEventHandlerId,
attributeFrame.AttributeName,
attributeFrame.AttributeValue,
attributeFrame.AttributeEventUpdatesAttributeName);
}
private void ApplyRemoveAttribute(string removedAttributeName)
{
if (_targetElement == null)
{
throw new InvalidOperationException($"Trying to remove attribute {removedAttributeName} to an adapter that isn't for an element");
}
_targetElement.ApplyAttribute(
attributeEventHandlerId: 0,
attributeName: removedAttributeName,
attributeValue: null,
attributeEventUpdatesAttributeName: null);
}
private int ApplyPrependFrame(RenderBatch batch, int componentId, int siblingIndex, RenderTreeFrame[] frames, int frameIndex)
{
ref var frame = ref frames[frameIndex];
switch (frame.FrameType)
{
case RenderTreeFrameType.Element:
{
InsertElement(siblingIndex, frames, frameIndex, componentId, batch);
return 1;
}
case RenderTreeFrameType.Component:
{
// Components are represented by NativeComponentAdapter
var childAdapter = Renderer.CreateAdapterForChildComponent(_targetElement ?? _closestPhysicalParent, frame.ComponentId);
childAdapter.Name = $"For: '{frame.Component.GetType().FullName}'";
childAdapter._targetComponent = frame.Component;
AddChildAdapter(siblingIndex, childAdapter);
return 1;
}
case RenderTreeFrameType.Region:
{
return InsertFrameRange(batch, componentId, siblingIndex, frames, frameIndex + 1, frameIndex + frame.RegionSubtreeLength);
}
case RenderTreeFrameType.Markup:
{
if (_targetElement is IHandleChildContentText handleChildContentText)
{
handleChildContentText.HandleText(siblingIndex, frame.MarkupContent);
}
else if (!string.IsNullOrWhiteSpace(frame.MarkupContent))
{
throw new NotImplementedException("Nonempty markup: " + frame.MarkupContent);
}
#pragma warning disable CA2000 // Dispose objects before losing scope; adapters are disposed when they are removed from the adapter tree
var childAdapter = CreateAdapter(_targetElement ?? _closestPhysicalParent);
#pragma warning restore CA2000 // Dispose objects before losing scope
childAdapter.Name = $"Markup, sib#={siblingIndex}";
AddChildAdapter(siblingIndex, childAdapter);
return 1;
}
case RenderTreeFrameType.Text:
{
if (_targetElement is IHandleChildContentText handleChildContentText)
{
handleChildContentText.HandleText(siblingIndex, frame.TextContent);
}
else if (!string.IsNullOrWhiteSpace(frame.TextContent))
{
throw new NotImplementedException("Nonempty text: " + frame.TextContent);
}
#pragma warning disable CA2000 // Dispose objects before losing scope; adapters are disposed when they are removed from the adapter tree
var childAdapter = CreateAdapter(_targetElement ?? _closestPhysicalParent);
#pragma warning restore CA2000 // Dispose objects before losing scope
childAdapter.Name = $"Text, sib#={siblingIndex}";
AddChildAdapter(siblingIndex, childAdapter);
return 1;
}
default:
throw new NotImplementedException($"Not supported frame type: {frame.FrameType}");
}
}
private NativeComponentAdapter CreateAdapter(IElementHandler physicalParent)
{
return new NativeComponentAdapter(Renderer, physicalParent);
}
private void InsertElement(int siblingIndex, RenderTreeFrame[] frames, int frameIndex, int componentId, RenderBatch batch)
{
// Elements represent native elements
ref var frame = ref frames[frameIndex];
//var elementName = frame.ElementName;
//var elementHandlerFactory = ElementHandlerRegistry.ElementHandlers[elementName];
var elementHandler = _targetComponent as IElementHandler;
//var elementHandler = elementHandlerFactory.CreateElementHandler(new ElementHandlerFactoryContext(Renderer, _closestPhysicalParent));
//if (_targetComponent is NativeControlComponentBase componentInstance)
//{
// componentInstance.SetElementReference(elementHandler);
//}
if (elementHandler is ICpfElementHandler handler)
{
handler.Renderer = Renderer;
handler.ComponentId = componentId;
}
if (siblingIndex != 0)
{
// With the current design, we should be able to ignore sibling indices for elements,
// so bail out if that's not the case
throw new NotSupportedException($"Currently we assume all adapter elements render exactly zero or one elements. Found an element with sibling index {siblingIndex}");
}
// TODO: Consider in the future calling a new API to check if the elementHandler represents a native UI component:
// if (Renderer.ElementManager.IsNativeElement(elementHandler)) { add to UI tree }
// else { do something with non-native element, e.g. notify parent to handle it }
// For the location in the physical UI tree, find the last preceding-sibling adapter that has
// a physical descendant (if any). If there is one, we physically insert after that one. If not,
// we'll insert as the first child of the closest physical parent.
if (!Renderer.ElementManager.IsParented(elementHandler))
{
var elementIndex = GetIndexForElement();
Renderer.ElementManager.AddChildElement(_closestPhysicalParent, elementHandler, elementIndex);
}
_targetElement = elementHandler;
var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength;
for (var descendantIndex = frameIndex + 1; descendantIndex < endIndexExcl; descendantIndex++)
{
var candidateFrame = frames[descendantIndex];
if (candidateFrame.FrameType == RenderTreeFrameType.Attribute)
{
ApplySetAttribute(ref candidateFrame);
}
else
{
// As soon as we see a non-attribute child, all the subsequent child frames are
// not attributes, so bail out and insert the remnants recursively
InsertFrameRange(batch, componentId, childIndex: 0, frames, descendantIndex, endIndexExcl);
break;
}
}
}
/// <summary>
/// Finds the sibling index to insert this adapter's element into. It walks up Parent adapters to find
/// an earlier sibling that has a native element, and uses that native element's physical index to determine
/// the location of the new element.
/// <code>
/// * Adapter0
/// * Adapter1
/// * Adapter2
/// * Adapter3 (native)
/// * Adapter3.0 (searchOrder=2)
/// * Adapter3.0.0 (searchOrder=3)
/// * Adapter3.0.1 (native) (searchOrder=4) &lt;-- This is the nearest earlier sibling that has a physical element)
/// * Adapter3.0.2
/// * Adapter3.1 (searchOrder=1)
/// * Adapter3.1.0 (searchOrder=0)
/// * Adapter3.1.1 (native) &lt;-- Current adapter
/// * Adapter3.1.2
/// * Adapter3.2
/// * Adapter4
/// </code>
/// </summary>
/// <returns>The index at which the native element should be inserted into within the parent. It returns -1 as a failure mode.</returns>
private int GetIndexForElement()
{
var childAdapter = this;
var parentAdapter = Parent;
while (parentAdapter != null)
{
// Walk previous siblings of this level and deep-scan them for native elements
var matchedEarlierSibling = GetEarlierSiblingMatch(parentAdapter, childAdapter);
if (matchedEarlierSibling != null)
{
if (!Renderer.ElementManager.IsParentOfChild(_closestPhysicalParent, matchedEarlierSibling._targetElement))
{
Debug.Fail($"Expected that the item found ({matchedEarlierSibling.DebugName}) with target element ({matchedEarlierSibling._targetElement.GetType().FullName}) should necessarily be an immediate child of the closest native parent ({_closestPhysicalParent.GetType().FullName}), but it wasn't...");
}
// If a native element was found somewhere within this sibling, the index for the new element
// will be 1 greater than its native index.
return Renderer.ElementManager.GetPhysicalSiblingIndex(matchedEarlierSibling._targetElement) + 1;
}
// If this level has a native element and all its relevant children have been scanned, then there's
// no previous sibling, so the new element to be added will be its earliest child (index=0). (There
// might be *later* siblings, but they are not relevant to this search.)
if (parentAdapter._targetElement != null)
{
Debug.Assert(parentAdapter._targetElement == _closestPhysicalParent, $"Expected that nearest parent ({parentAdapter.DebugName}) with native element ({parentAdapter._targetElement.GetType().FullName}) would have the closest physical parent ({_closestPhysicalParent.GetType().FullName}).");
return 0;
}
// If we haven't found a previous sibling with a native element or reached a native container, keep
// walking up the parent tree...
childAdapter = parentAdapter;
parentAdapter = parentAdapter.Parent;
}
Debug.Fail($"Expected to find a parent with a native element but found none.");
return -1;
}
private static NativeComponentAdapter GetEarlierSiblingMatch(NativeComponentAdapter parentAdapter, NativeComponentAdapter childAdapter)
{
var indexOfParentsChildAdapter = parentAdapter.Children.IndexOf(childAdapter);
for (var i = indexOfParentsChildAdapter - 1; i >= 0; i--)
{
var sibling = parentAdapter.Children[i];
if (sibling._targetElement is INonChildContainerElement)
{
continue;
}
// Deep scan this sibling adapter to find its latest and highest native element
var siblingWithNativeElement = sibling.GetLastDescendantWithPhysicalElement();
if (siblingWithNativeElement != null)
{
return siblingWithNativeElement;
}
}
// No preceding sibling has any native elements
return null;
}
private NativeComponentAdapter GetLastDescendantWithPhysicalElement()
{
if (_targetElement is INonChildContainerElement)
{
return null;
}
if (_targetElement != null)
{
// If this adapter has a target element, then this is the droid we're looking for. It can't be
// any children of this target element because they can't be children of this element's parent.
return this;
}
for (var i = Children.Count - 1; i >= 0; i--)
{
var child = Children[i];
var physicalDescendant = child.GetLastDescendantWithPhysicalElement();
if (physicalDescendant != null)
{
return physicalDescendant;
}
}
return null;
}
private int InsertFrameRange(RenderBatch batch, int componentId, int childIndex, RenderTreeFrame[] frames, int startIndex, int endIndexExcl)
{
var origChildIndex = childIndex;
for (var index = startIndex; index < endIndexExcl; index++)
{
ref var frame = ref batch.ReferenceFrames.Array[index];
var numChildrenInserted = ApplyPrependFrame(batch, componentId, childIndex, frames, index);
childIndex += numChildrenInserted;
// Skip over any descendants, since they are already dealt with recursively
index += CountDescendantFrames(frame);
}
return (childIndex - origChildIndex); // Total number of children inserted
}
private static int CountDescendantFrames(RenderTreeFrame frame)
{
return frame.FrameType switch
{
// The following frame types have a subtree length. Other frames may use that memory slot
// to mean something else, so we must not read it. We should consider having nominal subtypes
// of RenderTreeFramePointer that prevent access to non-applicable fields.
RenderTreeFrameType.Component => frame.ComponentSubtreeLength - 1,
RenderTreeFrameType.Element => frame.ElementSubtreeLength - 1,
RenderTreeFrameType.Region => frame.RegionSubtreeLength - 1,
_ => 0,
};
;
}
private void AddChildAdapter(int siblingIndex, NativeComponentAdapter childAdapter)
{
childAdapter.Parent = this;
if (siblingIndex <= Children.Count)
{
Children.Insert(siblingIndex, childAdapter);
}
else
{
Debug.WriteLine($"WARNING: {nameof(AddChildAdapter)} called with {nameof(siblingIndex)}={siblingIndex}, but Children.Count={Children.Count}");
Children.Add(childAdapter);
}
}
public void Dispose()
{
if (_targetElement is IDisposable disposableTargetElement)
{
disposableTargetElement.Dispose();
}
}
}
}