// 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 { /// /// Represents a "shadow" item that Blazor uses to map changes into the live native UI tree. /// [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 Children { get; } = new List(); private readonly IElementHandler _closestPhysicalParent; private IElementHandler _targetElement; private IComponent _targetComponent; public NativeComponentRenderer Renderer { get; } /// /// Used for debugging purposes. /// public string Name { get; internal set; } public override string ToString() { return $"{nameof(NativeComponentAdapter)}: Name={Name ?? ""}, Target={_targetElement?.GetType().Name ?? ""}, #Children={Children.Count}"; } internal void ApplyEdits(int componentId, ArrayBuilderSegment edits, ArrayRange 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; } } } /// /// 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. /// /// * Adapter0 /// * Adapter1 /// * Adapter2 /// * Adapter3 (native) /// * Adapter3.0 (searchOrder=2) /// * Adapter3.0.0 (searchOrder=3) /// * Adapter3.0.1 (native) (searchOrder=4) <-- 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) <-- Current adapter /// * Adapter3.1.2 /// * Adapter3.2 /// * Adapter4 /// /// /// The index at which the native element should be inserted into within the parent. It returns -1 as a failure mode. 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(); } } } }