mirror of
https://gitee.com/csharpui/CPF.git
synced 2025-12-21 11:00:02 +08:00
Razor测试
This commit is contained in:
44
CPF.Razor/Core/AttributesBuilder.cs
Normal file
44
CPF.Razor/Core/AttributesBuilder.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
// This wraps a RenderTreeBuilder in such a way that consumers
|
||||
// can only call the desired AddAttribute method, can't supply
|
||||
// sequence numbers, and can't leak the instance outside their
|
||||
// position in the call stack.
|
||||
|
||||
#pragma warning disable CA1815 // Override equals and operator equals on value types; these instances are never compared
|
||||
public readonly ref struct AttributesBuilder
|
||||
#pragma warning restore CA1815 // Override equals and operator equals on value types
|
||||
{
|
||||
private readonly RenderTreeBuilder _underlyingBuilder;
|
||||
|
||||
public AttributesBuilder(RenderTreeBuilder underlyingBuilder)
|
||||
{
|
||||
_underlyingBuilder = underlyingBuilder;
|
||||
}
|
||||
|
||||
public void AddAttribute(string name, object value)
|
||||
{
|
||||
// Using a fixed sequence number is allowed for attribute frames,
|
||||
// and causes the diff algorithm to use a dictionary to match old
|
||||
// and new values.
|
||||
_underlyingBuilder.AddAttribute(0, name, value);
|
||||
}
|
||||
|
||||
public void AddAttribute(string name, bool value)
|
||||
{
|
||||
// Using a fixed sequence number is allowed for attribute frames,
|
||||
// and causes the diff algorithm to use a dictionary to match old
|
||||
// and new values.
|
||||
|
||||
// bool values are converted to ints (which later become strings) to ensure that
|
||||
// all values are always rendered, not only 'true' values. This ensures that the
|
||||
// element handlers will see all property changes and can handle them as needed.
|
||||
_underlyingBuilder.AddAttribute(0, name, value ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
CPF.Razor/Core/ElementHandlerFactory.cs
Normal file
22
CPF.Razor/Core/ElementHandlerFactory.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace CPF.BlazorBindings
|
||||
{
|
||||
internal class ElementHandlerFactory
|
||||
{
|
||||
private readonly Func<NativeComponentRenderer, IElementHandler, IElementHandler> _callback;
|
||||
|
||||
public ElementHandlerFactory(Func<NativeComponentRenderer, IElementHandler, IElementHandler> callback)
|
||||
{
|
||||
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
}
|
||||
|
||||
public IElementHandler CreateElementHandler(ElementHandlerFactoryContext context)
|
||||
{
|
||||
return _callback(context.Renderer, context.ParentHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
CPF.Razor/Core/ElementHandlerFactoryContext.cs
Normal file
20
CPF.Razor/Core/ElementHandlerFactoryContext.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace CPF.BlazorBindings
|
||||
{
|
||||
internal class ElementHandlerFactoryContext
|
||||
{
|
||||
public ElementHandlerFactoryContext(NativeComponentRenderer renderer, IElementHandler parentHandler)
|
||||
{
|
||||
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
ParentHandler = parentHandler;
|
||||
}
|
||||
|
||||
public IElementHandler ParentHandler { get; }
|
||||
|
||||
public NativeComponentRenderer Renderer { get; }
|
||||
}
|
||||
}
|
||||
31
CPF.Razor/Core/ElementHandlerRegistry.cs
Normal file
31
CPF.Razor/Core/ElementHandlerRegistry.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CPF.BlazorBindings
|
||||
{
|
||||
public static class ElementHandlerRegistry
|
||||
{
|
||||
//internal static Dictionary<string, ElementHandlerFactory> ElementHandlers { get; }
|
||||
// = new Dictionary<string, ElementHandlerFactory>();
|
||||
|
||||
//public static void RegisterElementHandler<TComponent>(
|
||||
// Func<NativeComponentRenderer, IElementHandler, IElementHandler> factory) where TComponent : NativeControlComponentBase
|
||||
//{
|
||||
// ElementHandlers.Add(typeof(TComponent).FullName, new ElementHandlerFactory(factory));
|
||||
//}
|
||||
|
||||
//public static void RegisterElementHandler<TComponent>(
|
||||
// Func<NativeComponentRenderer, IElementHandler> factory) where TComponent : NativeControlComponentBase
|
||||
//{
|
||||
// ElementHandlers.Add(typeof(TComponent).FullName, new ElementHandlerFactory((renderer, _) => factory(renderer)));
|
||||
//}
|
||||
|
||||
//public static void RegisterElementHandler<TComponent, TControlHandler>() where TComponent : NativeControlComponentBase where TControlHandler : class, IElementHandler, new()
|
||||
//{
|
||||
// RegisterElementHandler<TComponent>((_, __) => new TControlHandler());
|
||||
//}
|
||||
}
|
||||
}
|
||||
20
CPF.Razor/Core/ElementManager.cs
Normal file
20
CPF.Razor/Core/ElementManager.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Utilities needed by the system to manage native controls. Implementations
|
||||
/// of native rendering systems have their own quirks in terms of dealing with
|
||||
/// parent/child relationships, so each must implement this given the constraints
|
||||
/// and requirements of their systems.
|
||||
/// </summary>
|
||||
public abstract class ElementManager
|
||||
{
|
||||
public abstract void AddChildElement(IElementHandler parentHandler, IElementHandler childHandler, int physicalSiblingIndex);
|
||||
public abstract int GetPhysicalSiblingIndex(IElementHandler handler);
|
||||
public abstract bool IsParented(IElementHandler handler);
|
||||
public abstract bool IsParentOfChild(IElementHandler parentHandler, IElementHandler childHandler);
|
||||
public abstract void RemoveElement(IElementHandler handler);
|
||||
}
|
||||
}
|
||||
54
CPF.Razor/Core/ElementManagerOfElementType.cs
Normal file
54
CPF.Razor/Core/ElementManagerOfElementType.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility intermediate class to make it easier to strongly-type a derived <see cref="ElementManager"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElementType"></typeparam>
|
||||
public abstract class ElementManager<TElementType> : ElementManager
|
||||
{
|
||||
private static TElementType ConvertToType(IElementHandler elementHandler, string parameterName)
|
||||
{
|
||||
if (!(elementHandler is TElementType))
|
||||
{
|
||||
throw new ArgumentException($"Expected parameter value of type '{elementHandler.GetType().FullName}' to be convertible to type '{typeof(TElementType).FullName}'.", parameterName);
|
||||
}
|
||||
return (TElementType)elementHandler;
|
||||
}
|
||||
|
||||
public sealed override void AddChildElement(IElementHandler parentHandler, IElementHandler childHandler, int physicalSiblingIndex)
|
||||
{
|
||||
AddChildElement(ConvertToType(parentHandler, nameof(parentHandler)), ConvertToType(childHandler, nameof(childHandler)), physicalSiblingIndex);
|
||||
}
|
||||
|
||||
public sealed override int GetPhysicalSiblingIndex(IElementHandler handler)
|
||||
{
|
||||
return GetPhysicalSiblingIndex(ConvertToType(handler, nameof(handler)));
|
||||
}
|
||||
|
||||
public sealed override bool IsParented(IElementHandler handler)
|
||||
{
|
||||
return IsParented(ConvertToType(handler, nameof(handler)));
|
||||
}
|
||||
|
||||
public sealed override bool IsParentOfChild(IElementHandler parentHandler, IElementHandler childHandler)
|
||||
{
|
||||
return IsParentOfChild(ConvertToType(parentHandler, nameof(parentHandler)), ConvertToType(childHandler, nameof(childHandler)));
|
||||
}
|
||||
|
||||
public sealed override void RemoveElement(IElementHandler handler)
|
||||
{
|
||||
RemoveElement(ConvertToType(handler, nameof(handler)));
|
||||
}
|
||||
|
||||
protected abstract void AddChildElement(TElementType elementType1, TElementType elementType2, int physicalSiblingIndex);
|
||||
protected abstract int GetPhysicalSiblingIndex(TElementType elementType);
|
||||
protected abstract bool IsParented(TElementType elementType);
|
||||
protected abstract bool IsParentOfChild(TElementType elementType1, TElementType elementType2);
|
||||
protected abstract void RemoveElement(TElementType elementType);
|
||||
}
|
||||
}
|
||||
27
CPF.Razor/Core/IElementHandler.cs
Normal file
27
CPF.Razor/Core/IElementHandler.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a container for native element.
|
||||
/// </summary>
|
||||
public interface IElementHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets an attribute named <paramref name="attributeName"/> on the <see cref="TargetElement"/> represented by
|
||||
/// this handler to value <paramref name="attributeValue"/>.
|
||||
/// </summary>
|
||||
/// <param name="attributeEventHandlerId"></param>
|
||||
/// <param name="attributeName"></param>
|
||||
/// <param name="attributeValue"></param>
|
||||
/// <param name="attributeEventUpdatesAttributeName"></param>
|
||||
void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName);
|
||||
|
||||
/// <summary>
|
||||
/// The native element represented by this handler. This is often a native UI component, but can be any type
|
||||
/// of component used by the native system.
|
||||
/// </summary>
|
||||
object TargetElement { get; }
|
||||
}
|
||||
}
|
||||
18
CPF.Razor/Core/IHandleChildContentText.cs
Normal file
18
CPF.Razor/Core/IHandleChildContentText.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a mechanism for an <see cref="IElementHandler"/> to accept inline text.
|
||||
/// </summary>
|
||||
public interface IHandleChildContentText
|
||||
{
|
||||
/// <summary>
|
||||
/// This method is called to process inline text found in a component.
|
||||
/// </summary>
|
||||
/// <param name="index">the index of the string within a group of text strings.</param>
|
||||
/// <param name="text">The text to handle. This text may contain whitespace at the start and end of the string.</param>
|
||||
void HandleText(int index, string text);
|
||||
}
|
||||
}
|
||||
16
CPF.Razor/Core/INonChildContainerElement.cs
Normal file
16
CPF.Razor/Core/INonChildContainerElement.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker interface to indicate that this element is a container of elements that are not
|
||||
/// true children of their parent. For example, a host for elements that go in a modal dialog
|
||||
/// are not true children of their parent.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1040 // Avoid empty interfaces
|
||||
public interface INonChildContainerElement
|
||||
#pragma warning restore CA1040 // Avoid empty interfaces
|
||||
{
|
||||
}
|
||||
}
|
||||
465
CPF.Razor/Core/NativeComponentAdapter.cs
Normal file
465
CPF.Razor/Core/NativeComponentAdapter.cs
Normal file
@@ -0,0 +1,465 @@
|
||||
// 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 (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) <-- 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
|
||||
/// </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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
134
CPF.Razor/Core/NativeComponentRenderer.cs
Normal file
134
CPF.Razor/Core/NativeComponentRenderer.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
public abstract class NativeComponentRenderer : Renderer
|
||||
{
|
||||
private readonly Dictionary<int, NativeComponentAdapter> _componentIdToAdapter = new Dictionary<int, NativeComponentAdapter>();
|
||||
private ElementManager _elementManager;
|
||||
private readonly Dictionary<ulong, Action<ulong>> _eventRegistrations = new Dictionary<ulong, Action<ulong>>();
|
||||
|
||||
|
||||
public NativeComponentRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
|
||||
: base(serviceProvider, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
protected abstract ElementManager CreateNativeControlManager();
|
||||
|
||||
internal ElementManager ElementManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return _elementManager ?? (_elementManager = CreateNativeControlManager());
|
||||
}
|
||||
}
|
||||
|
||||
public override Dispatcher Dispatcher { get; }
|
||||
= Dispatcher.CreateDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a component of type <typeparamref name="TComponent"/> and adds it as a child of <paramref name="parent"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent"></typeparam>
|
||||
/// <param name="parent"></param>
|
||||
/// <returns></returns>
|
||||
public async Task AddComponent<TComponent>(IElementHandler parent) where TComponent : IComponent
|
||||
{
|
||||
await AddComponent(typeof(TComponent), parent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a component of type <paramref name="componentType"/> and adds it as a child of <paramref name="parent"/>.
|
||||
/// </summary>
|
||||
/// <param name="componentType"></param>
|
||||
/// <param name="parent"></param>
|
||||
/// <returns></returns>
|
||||
public async Task AddComponent(Type componentType, IElementHandler parent)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
var component = InstantiateComponent(componentType);
|
||||
var componentId = AssignRootComponentId(component);
|
||||
|
||||
var rootAdapter = new NativeComponentAdapter(this, closestPhysicalParent: parent, knownTargetElement: parent)
|
||||
{
|
||||
Name = $"RootAdapter attached to {parent.GetType().FullName}",
|
||||
};
|
||||
|
||||
_componentIdToAdapter[componentId] = rootAdapter;
|
||||
|
||||
await RenderRootComponentAsync(componentId).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
|
||||
{
|
||||
foreach (var updatedComponent in renderBatch.UpdatedComponents.Array.Take(renderBatch.UpdatedComponents.Count))
|
||||
{
|
||||
var adapter = _componentIdToAdapter[updatedComponent.ComponentId];
|
||||
adapter.ApplyEdits(updatedComponent.ComponentId, updatedComponent.Edits, renderBatch.ReferenceFrames, renderBatch);
|
||||
}
|
||||
|
||||
var numDisposedComponents = renderBatch.DisposedComponentIDs.Count;
|
||||
for (var i = 0; i < numDisposedComponents; i++)
|
||||
{
|
||||
var disposedComponentId = renderBatch.DisposedComponentIDs.Array[i];
|
||||
if (_componentIdToAdapter.TryGetValue(disposedComponentId, out var adapter))
|
||||
{
|
||||
_componentIdToAdapter.Remove(disposedComponentId);
|
||||
(adapter as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
var numDisposeEventHandlers = renderBatch.DisposedEventHandlerIDs.Count;
|
||||
if (numDisposeEventHandlers != 0)
|
||||
{
|
||||
for (var i = 0; i < numDisposeEventHandlers; i++)
|
||||
{
|
||||
DisposeEvent(renderBatch.DisposedEventHandlerIDs.Array[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void RegisterEvent(ulong eventHandlerId, Action<ulong> unregisterCallback)
|
||||
{
|
||||
if (eventHandlerId == 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(eventHandlerId), "Event handler ID must not be 0.");
|
||||
}
|
||||
if (unregisterCallback == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(unregisterCallback));
|
||||
}
|
||||
_eventRegistrations.Add(eventHandlerId, unregisterCallback);
|
||||
}
|
||||
|
||||
private void DisposeEvent(ulong eventHandlerId)
|
||||
{
|
||||
if (!_eventRegistrations.TryGetValue(eventHandlerId, out var unregisterCallback))
|
||||
{
|
||||
throw new InvalidOperationException($"Attempting to dispose unknown event handler id '{eventHandlerId}'.");
|
||||
}
|
||||
unregisterCallback(eventHandlerId);
|
||||
}
|
||||
|
||||
internal NativeComponentAdapter CreateAdapterForChildComponent(IElementHandler physicalParent, int componentId)
|
||||
{
|
||||
var result = new NativeComponentAdapter(this, physicalParent);
|
||||
_componentIdToAdapter[componentId] = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
CPF.Razor/Core/NativeControlComponentBase.cs
Normal file
68
CPF.Razor/Core/NativeControlComponentBase.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using System;
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
public abstract class NativeControlComponentBase<T> : ComponentBase, ICpfElementHandler where T : UIElement, new()
|
||||
{
|
||||
public IElementHandler ElementHandler { get; private set; }
|
||||
|
||||
UIElement ICpfElementHandler.Element => Element;
|
||||
|
||||
T element;
|
||||
public T Element
|
||||
{
|
||||
get
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
element = CreateElement();
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
public object TargetElement => Element;
|
||||
|
||||
//public void SetElementReference(IElementHandler elementHandler)
|
||||
//{
|
||||
// ElementHandler = elementHandler ?? throw new ArgumentNullException(nameof(elementHandler));
|
||||
//}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
if (builder is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.OpenElement(0, GetType().FullName);
|
||||
RenderAttributes(new AttributesBuilder(builder));
|
||||
|
||||
var childContent = GetChildContent();
|
||||
if (childContent != null)
|
||||
{
|
||||
builder.AddContent(2, childContent);
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
protected virtual void RenderAttributes(AttributesBuilder builder)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual RenderFragment GetChildContent() => null;
|
||||
|
||||
public abstract void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName);
|
||||
|
||||
protected virtual T CreateElement()
|
||||
{
|
||||
return new T();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public static class ServiceCollectionAdditionalServicesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Copies service descriptors from one service collection to another.
|
||||
/// </summary>
|
||||
/// <param name="services">The destination service collection to which the additional services will be added.</param>
|
||||
/// <param name="additionalServices">The list of additional services to add.</param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddAdditionalServices(this IServiceCollection services, IServiceCollection additionalServices)
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (additionalServices is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(additionalServices));
|
||||
}
|
||||
|
||||
foreach (var additionalService in additionalServices)
|
||||
{
|
||||
services.Add(additionalService);
|
||||
}
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
CPF.Razor/Core/TextSpanContainer.cs
Normal file
51
CPF.Razor/Core/TextSpanContainer.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CPF.Razor
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for types that accept inline text spans. This type collects text spans
|
||||
/// and returns the string represented by the contained text spans.
|
||||
/// </summary>
|
||||
public class TextSpanContainer
|
||||
{
|
||||
private readonly List<string> _textSpans = new List<string>();
|
||||
|
||||
public TextSpanContainer(bool trimWhitespace = true)
|
||||
{
|
||||
TrimWhitespace = trimWhitespace;
|
||||
}
|
||||
|
||||
public bool TrimWhitespace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the text spans with the new text at the new index and returns the new
|
||||
/// string represented by the contained text spans.
|
||||
/// </summary>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
public string GetUpdatedText(int index, string text)
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
if (index >= _textSpans.Count)
|
||||
{
|
||||
// Expand the list to allow for the new text's index to exist
|
||||
_textSpans.AddRange(new string[index - _textSpans.Count + 1]);
|
||||
}
|
||||
_textSpans[index] = text;
|
||||
|
||||
var allText = string.Join(string.Empty, _textSpans);
|
||||
return TrimWhitespace
|
||||
? allText?.Trim()
|
||||
: allText;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user