diff --git a/CPF.Razor/CPF.Razor.csproj b/CPF.Razor/CPF.Razor.csproj new file mode 100644 index 0000000..449f4b6 --- /dev/null +++ b/CPF.Razor/CPF.Razor.csproj @@ -0,0 +1,37 @@ + + + + netstandard2.0 + 9.0 + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/CPF.Razor/Controls/Element.cs b/CPF.Razor/Controls/Element.cs new file mode 100644 index 0000000..fd6fa35 --- /dev/null +++ b/CPF.Razor/Controls/Element.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Components; +//using Microsoft.MobileBlazorBindings.Core; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CPF.Razor.Controls +{ + public abstract class Element : NativeControlComponentBase where T : UIElement, new() + { + [Parameter] public string MarginLeft { get; set; } + [Parameter] public string MarginTop { get; set; } + [Parameter] public string Width { get; set; } + [Parameter] public string Height { get; set; } + + //public CPF.UIElement NativeControl => ((ICpfElementHandler)ElementHandler).Element; + + protected override void RenderAttributes(AttributesBuilder builder) + { + base.RenderAttributes(builder); + + if (MarginLeft != null) + { + builder.AddAttribute(nameof(MarginLeft), MarginLeft); + } + if (MarginTop != null) + { + builder.AddAttribute(nameof(MarginTop), MarginTop); + } + if (Height != null) + { + builder.AddAttribute(nameof(Height), Height); + } + if (Width != null) + { + builder.AddAttribute(nameof(Width), Width); + } + } + } +} diff --git a/CPF.Razor/Controls/Panel.cs b/CPF.Razor/Controls/Panel.cs new file mode 100644 index 0000000..a49199f --- /dev/null +++ b/CPF.Razor/Controls/Panel.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Components; +//using Microsoft.MobileBlazorBindings.Core; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CPF.Razor.Controls +{ + public partial class Panel : Element + { + //static Panel() + //{ + // ElementHandlerRegistry.RegisterElementHandler(); + //} + + [Parameter] public string Background { get; set; } + +#pragma warning disable CA1721 // Property names should not match get methods + [Parameter] public RenderFragment ChildContent { get; set; } +#pragma warning restore CA1721 // Property names should not match get methods + protected override void RenderAttributes(AttributesBuilder builder) + { + base.RenderAttributes(builder); + + if (Background != null) + { + builder.AddAttribute(nameof(Background), Background); + } + } + protected override RenderFragment GetChildContent() => ChildContent; + + public override void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName) + { + //switch (attributeName) + //{ + // //case nameof(AutoScroll): + // // AutoScroll = AttributeHelper.GetBool(attributeValue); + // // break; + // default: + + // break; + //} + var p = Element.GetPropertyMetadata(attributeName); + if (p != null) + { + Element.SetValue(attributeValue.ConvertTo(p.PropertyType), attributeName); + } + } + } +} diff --git a/CPF.Razor/Controls/TestElement.cs b/CPF.Razor/Controls/TestElement.cs new file mode 100644 index 0000000..c24aa36 --- /dev/null +++ b/CPF.Razor/Controls/TestElement.cs @@ -0,0 +1,153 @@ +using Microsoft.AspNetCore.Components; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using System.Threading.Tasks; + +namespace CPF.Razor.Controls +{ + public class TestElement : Microsoft.AspNetCore.Components.IComponent, IHandleEvent, IHandleAfterRender, ICustomTypeDescriptor + { + [Parameter] + public string Test { get; set; } + + public void Attach(RenderHandle renderHandle) + { + throw new NotImplementedException(); + } + + public AttributeCollection GetAttributes() + { + throw new NotImplementedException(); + } + + public string GetClassName() + { + return "TestElement"; + } + + public string GetComponentName() + { + return "GetComponentName"; + } + + public TypeConverter GetConverter() + { + throw new NotImplementedException(); + } + + public EventDescriptor GetDefaultEvent() + { + throw new NotImplementedException(); + } + + public PropertyDescriptor GetDefaultProperty() + { + throw new NotImplementedException(); + } + + public object GetEditor(Type editorBaseType) + { + throw new NotImplementedException(); + } + + public EventDescriptorCollection GetEvents() + { + throw new NotImplementedException(); + } + + public EventDescriptorCollection GetEvents(Attribute[] attributes) + { + throw new NotImplementedException(); + } + + public PropertyDescriptorCollection GetProperties() + { + return new PropertyDescriptorCollection(new CpfPropertyDescriptor[] { new CpfPropertyDescriptor("Pro1", false, typeof(string), null) }); + } + + public PropertyDescriptorCollection GetProperties(Attribute[] attributes) + { + throw new NotImplementedException(); + } + + public object GetPropertyOwner(PropertyDescriptor pd) + { + throw new NotImplementedException(); + } + + public Task HandleEventAsync(EventCallbackWorkItem item, object arg) + { + throw new NotImplementedException(); + } + + public Task OnAfterRenderAsync() + { + throw new NotImplementedException(); + } + + public Task SetParametersAsync(ParameterView parameters) + { + throw new NotImplementedException(); + } + } + class CpfPropertyDescriptor : PropertyDescriptor + { + public CpfPropertyDescriptor(string name, bool readOnly, Type type, Attribute[] attributes) : base(name, attributes) + { + isreadonly = readOnly; + pType = type; + } + public string FileTypes { get; set; } + public bool IsAttached { get; set; } + public bool IsDependency { get; set; } + + bool isreadonly; + Type pType; + public override Type ComponentType => typeof(TestElement); + + public override bool IsReadOnly => isreadonly; + + public override Type PropertyType => pType; + + public override bool CanResetValue(object component) + { + return false; + } + + public override object GetValue(object component) + { + if (component is UIElement element) + { + return element.GetPropretyValue(Name); + } + return null; + } + + public override void ResetValue(object component) + { + //if (component is UIElement element) + //{ + // element.ResetValue(Name); + //} + } + + public override void SetValue(object component, object value) + { + if (component is UIElement element) + { + element.SetPropretyValue(Name, value); + } + } + + public override bool ShouldSerializeValue(object component) + { + return false; + } + public override string ToString() + { + return this.Name; + } + } +} diff --git a/CPF.Razor/Core/AttributesBuilder.cs b/CPF.Razor/Core/AttributesBuilder.cs new file mode 100644 index 0000000..159649f --- /dev/null +++ b/CPF.Razor/Core/AttributesBuilder.cs @@ -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); + } + } +} diff --git a/CPF.Razor/Core/ElementHandlerFactory.cs b/CPF.Razor/Core/ElementHandlerFactory.cs new file mode 100644 index 0000000..3876ca8 --- /dev/null +++ b/CPF.Razor/Core/ElementHandlerFactory.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; + +namespace CPF.BlazorBindings +{ + internal class ElementHandlerFactory + { + private readonly Func _callback; + + public ElementHandlerFactory(Func callback) + { + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + public IElementHandler CreateElementHandler(ElementHandlerFactoryContext context) + { + return _callback(context.Renderer, context.ParentHandler); + } + } +} diff --git a/CPF.Razor/Core/ElementHandlerFactoryContext.cs b/CPF.Razor/Core/ElementHandlerFactoryContext.cs new file mode 100644 index 0000000..98a6607 --- /dev/null +++ b/CPF.Razor/Core/ElementHandlerFactoryContext.cs @@ -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; } + } +} diff --git a/CPF.Razor/Core/ElementHandlerRegistry.cs b/CPF.Razor/Core/ElementHandlerRegistry.cs new file mode 100644 index 0000000..e26dfec --- /dev/null +++ b/CPF.Razor/Core/ElementHandlerRegistry.cs @@ -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 ElementHandlers { get; } + // = new Dictionary(); + + //public static void RegisterElementHandler( + // Func factory) where TComponent : NativeControlComponentBase + //{ + // ElementHandlers.Add(typeof(TComponent).FullName, new ElementHandlerFactory(factory)); + //} + + //public static void RegisterElementHandler( + // Func factory) where TComponent : NativeControlComponentBase + //{ + // ElementHandlers.Add(typeof(TComponent).FullName, new ElementHandlerFactory((renderer, _) => factory(renderer))); + //} + + //public static void RegisterElementHandler() where TComponent : NativeControlComponentBase where TControlHandler : class, IElementHandler, new() + //{ + // RegisterElementHandler((_, __) => new TControlHandler()); + //} + } +} diff --git a/CPF.Razor/Core/ElementManager.cs b/CPF.Razor/Core/ElementManager.cs new file mode 100644 index 0000000..3f7050e --- /dev/null +++ b/CPF.Razor/Core/ElementManager.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace CPF.Razor +{ + /// + /// 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. + /// + 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); + } +} diff --git a/CPF.Razor/Core/ElementManagerOfElementType.cs b/CPF.Razor/Core/ElementManagerOfElementType.cs new file mode 100644 index 0000000..5aa2fc5 --- /dev/null +++ b/CPF.Razor/Core/ElementManagerOfElementType.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; + +namespace CPF.Razor +{ + /// + /// Utility intermediate class to make it easier to strongly-type a derived . + /// + /// + public abstract class ElementManager : 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); + } +} diff --git a/CPF.Razor/Core/IElementHandler.cs b/CPF.Razor/Core/IElementHandler.cs new file mode 100644 index 0000000..c340f03 --- /dev/null +++ b/CPF.Razor/Core/IElementHandler.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace CPF.Razor +{ + /// + /// Represents a container for native element. + /// + public interface IElementHandler + { + /// + /// Sets an attribute named on the represented by + /// this handler to value . + /// + /// + /// + /// + /// + void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName); + + /// + /// 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. + /// + object TargetElement { get; } + } +} diff --git a/CPF.Razor/Core/IHandleChildContentText.cs b/CPF.Razor/Core/IHandleChildContentText.cs new file mode 100644 index 0000000..a58b2bb --- /dev/null +++ b/CPF.Razor/Core/IHandleChildContentText.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace CPF.Razor +{ + /// + /// Defines a mechanism for an to accept inline text. + /// + public interface IHandleChildContentText + { + /// + /// This method is called to process inline text found in a component. + /// + /// the index of the string within a group of text strings. + /// The text to handle. This text may contain whitespace at the start and end of the string. + void HandleText(int index, string text); + } +} diff --git a/CPF.Razor/Core/INonChildContainerElement.cs b/CPF.Razor/Core/INonChildContainerElement.cs new file mode 100644 index 0000000..e3459b5 --- /dev/null +++ b/CPF.Razor/Core/INonChildContainerElement.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace CPF.Razor +{ + /// + /// 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. + /// +#pragma warning disable CA1040 // Avoid empty interfaces + public interface INonChildContainerElement +#pragma warning restore CA1040 // Avoid empty interfaces + { + } +} diff --git a/CPF.Razor/Core/NativeComponentAdapter.cs b/CPF.Razor/Core/NativeComponentAdapter.cs new file mode 100644 index 0000000..1658948 --- /dev/null +++ b/CPF.Razor/Core/NativeComponentAdapter.cs @@ -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 +{ + /// + /// 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 (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(); + } + } + } +} diff --git a/CPF.Razor/Core/NativeComponentRenderer.cs b/CPF.Razor/Core/NativeComponentRenderer.cs new file mode 100644 index 0000000..1cb7beb --- /dev/null +++ b/CPF.Razor/Core/NativeComponentRenderer.cs @@ -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 _componentIdToAdapter = new Dictionary(); + private ElementManager _elementManager; + private readonly Dictionary> _eventRegistrations = new Dictionary>(); + + + 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(); + + /// + /// Creates a component of type and adds it as a child of . + /// + /// + /// + /// + public async Task AddComponent(IElementHandler parent) where TComponent : IComponent + { + await AddComponent(typeof(TComponent), parent).ConfigureAwait(false); + } + + /// + /// Creates a component of type and adds it as a child of . + /// + /// + /// + /// + 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 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; + } + } +} diff --git a/CPF.Razor/Core/NativeControlComponentBase.cs b/CPF.Razor/Core/NativeControlComponentBase.cs new file mode 100644 index 0000000..79f0ac9 --- /dev/null +++ b/CPF.Razor/Core/NativeControlComponentBase.cs @@ -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 : 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(); + } + } +} diff --git a/CPF.Razor/Core/ServiceCollectionAdditionalServicesExtensions.cs b/CPF.Razor/Core/ServiceCollectionAdditionalServicesExtensions.cs new file mode 100644 index 0000000..63c0075 --- /dev/null +++ b/CPF.Razor/Core/ServiceCollectionAdditionalServicesExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionAdditionalServicesExtensions + { + /// + /// Copies service descriptors from one service collection to another. + /// + /// The destination service collection to which the additional services will be added. + /// The list of additional services to add. + /// + 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; + } + } +} diff --git a/CPF.Razor/Core/TextSpanContainer.cs b/CPF.Razor/Core/TextSpanContainer.cs new file mode 100644 index 0000000..bbb31ec --- /dev/null +++ b/CPF.Razor/Core/TextSpanContainer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; + +namespace CPF.Razor +{ + /// + /// Helper class for types that accept inline text spans. This type collects text spans + /// and returns the string represented by the contained text spans. + /// + public class TextSpanContainer + { + private readonly List _textSpans = new List(); + + public TextSpanContainer(bool trimWhitespace = true) + { + TrimWhitespace = trimWhitespace; + } + + public bool TrimWhitespace { get; } + + /// + /// Updates the text spans with the new text at the new index and returns the new + /// string represented by the contained text spans. + /// + /// + /// + /// + 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; + } + } +} diff --git a/CPF.Razor/CpfDispatcher.cs b/CPF.Razor/CpfDispatcher.cs new file mode 100644 index 0000000..f5a3e6c --- /dev/null +++ b/CPF.Razor/CpfDispatcher.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Components; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace CPF.Razor +{ + public class CpfDispatcher : Dispatcher + { + public override bool CheckAccess() + { + return CPF.Threading.Dispatcher.MainThread.CheckAccess(); + } + + public override Task InvokeAsync(Action workItem) + { + return Task.Run(() => + { + CPF.Threading.Dispatcher.MainThread.Invoke(workItem); + }); + } + + public override Task InvokeAsync(Func workItem) + { + return Task.Run(() => + { + var task = Task.CompletedTask; + CPF.Threading.Dispatcher.MainThread.Invoke(() => { task = workItem(); }); + return task; + }); + } + + public override Task InvokeAsync(Func workItem) + { + return Task.Run(() => + { + TResult result = default; + CPF.Threading.Dispatcher.MainThread.Invoke(() => { result = workItem(); }); + return result; + }); + } + + public override Task InvokeAsync(Func> workItem) + { + return Task.Run(() => + { + TResult result = default; + CPF.Threading.Dispatcher.MainThread.Invoke(async () => { result = await workItem(); }); + return result; + }); + } + } +} diff --git a/CPF.Razor/CpfElementManager.cs b/CPF.Razor/CpfElementManager.cs new file mode 100644 index 0000000..9dab84c --- /dev/null +++ b/CPF.Razor/CpfElementManager.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//using Microsoft.MobileBlazorBindings.Core; +using System; +using System.Diagnostics; + +namespace CPF.Razor +{ + internal class CpfElementManager : ElementManager + { + protected override bool IsParented(ICpfElementHandler handler) + { + return handler.Element.Parent != null; + } + + protected override void AddChildElement( + ICpfElementHandler parentHandler, + ICpfElementHandler childHandler, + int physicalSiblingIndex) + { + if (parentHandler.Element is CPF.Controls.Panel panel) + { + if (physicalSiblingIndex <= panel.Children.Count) + { + panel.Children.Insert(physicalSiblingIndex, childHandler.Element); + } + else + { + //Debug.WriteLine($"WARNING: {nameof(AddChildElement)} called with {nameof(physicalSiblingIndex)}={physicalSiblingIndex}, but parentControl.Controls.Count={parentHandler.Control.Controls.Count}"); + panel.Children.Add(childHandler.Element); + } + } + else if (parentHandler.Element is CPF.Controls.Window win) + { + if (physicalSiblingIndex <= win.Children.Count) + { + win.Children.Insert(physicalSiblingIndex, childHandler.Element); + } + else + { + win.Children.Add(childHandler.Element); + } + } + else if (parentHandler.Element is CPF.Controls.ContentControl contentControl) + { + contentControl.Content = childHandler.Element; + } + else + { + Debug.Fail("未实现添加控件"); + } + } + + protected override int GetPhysicalSiblingIndex( + ICpfElementHandler handler) + { + return (handler.Element.Parent as CPF.Controls.Panel).Children.IndexOf(handler.Element); + } + + protected override void RemoveElement(ICpfElementHandler handler) + { + if (handler.Element.Parent is CPF.Controls.Panel panel) + { + panel.Children.Remove(handler.Element); + } + else + { + Debug.Fail("未实现移除控件"); + } + } + + protected override bool IsParentOfChild(ICpfElementHandler parentHandler, ICpfElementHandler childHandler) + { + return childHandler.Element.Parent == parentHandler.Element; + } + } +} diff --git a/CPF.Razor/CpfExtensions.cs b/CPF.Razor/CpfExtensions.cs new file mode 100644 index 0000000..c852625 --- /dev/null +++ b/CPF.Razor/CpfExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; + +namespace CPF.Razor +{ + public static class CpfExtensions + { + /// + /// Creates a component of type and adds it as a child of . + /// + /// + /// + /// + public static void AddComponent(this IHost host, CPF.UIElement parent) where TComponent : IComponent + { + if (host is null) + { + throw new ArgumentNullException(nameof(host)); + } + + if (parent is null) + { + throw new ArgumentNullException(nameof(parent)); + } + + var services = host.Services; + var renderer = new CpfRenderer(services, services.GetRequiredService()); + + //// TODO: This call is an async call, but is called as "fire-and-forget," which is not ideal. + //// We need to figure out how to get Xamarin.Forms to run this startup code asynchronously, which + //// is how this method should be called. + renderer.AddComponent(new ElementHandler(renderer, parent)).ConfigureAwait(false); + } + } +} diff --git a/CPF.Razor/CpfHost.cs b/CPF.Razor/CpfHost.cs new file mode 100644 index 0000000..8c32046 --- /dev/null +++ b/CPF.Razor/CpfHost.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.IO; + +namespace CPF.Razor +{ + public static class CpfHost + { + public static IHostBuilder CreateDefaultBuilder() + { + // Inspired by Microsoft.Extensions.Hosting.Host, which can be seen here: + // https://github.com/dotnet/extensions/blob/master/src/Hosting/Hosting/src/Host.cs + // But slightly modified to work on all of Android, iOS, and UWP. + + var builder = new HostBuilder(); + + builder.UseContentRoot(Directory.GetCurrentDirectory()); + + builder.ConfigureLogging((hostingContext, logging) => + { + logging.AddConsole(configure => configure.DisableColors = true); + logging.AddDebug(); + logging.AddEventSourceLogger(); + }) + .UseDefaultServiceProvider((context, options) => + { + var isDevelopment = context.HostingEnvironment.IsDevelopment(); + options.ValidateScopes = isDevelopment; + options.ValidateOnBuild = isDevelopment; + }); + + return builder; + } + } +} diff --git a/CPF.Razor/CpfRenderer.cs b/CPF.Razor/CpfRenderer.cs new file mode 100644 index 0000000..6970d4b --- /dev/null +++ b/CPF.Razor/CpfRenderer.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Logging; +using System; +using Microsoft.AspNetCore.Components; +using System.Diagnostics; +//using Microsoft.MobileBlazorBindings.Core; + +namespace CPF.Razor +{ + public class CpfRenderer : NativeComponentRenderer + { + public CpfRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) + : base(serviceProvider, loggerFactory) + { + } + + protected override void HandleException(Exception exception) + { + //MessageBox.Show(exception?.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + Debug.WriteLine(exception?.Message); + } + + protected override ElementManager CreateNativeControlManager() + { + return new CpfElementManager(); + } + + public override Dispatcher Dispatcher => new CpfDispatcher(); + } +} diff --git a/CPF.Razor/ElementHandler.cs b/CPF.Razor/ElementHandler.cs new file mode 100644 index 0000000..f6975b0 --- /dev/null +++ b/CPF.Razor/ElementHandler.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CPF.Razor +{ + public class ElementHandler : ICpfElementHandler + { + public ElementHandler(NativeComponentRenderer renderer, CPF.UIElement elementControl) + { + Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); + Element = elementControl ?? throw new ArgumentNullException(nameof(elementControl)); + } + + //protected void RegisterEvent(string eventName, Action setId, Action clearId) + //{ + // RegisteredEvents[eventName] = new EventRegistration(eventName, setId, clearId); + //} + //private Dictionary RegisteredEvents { get; } = new Dictionary(); + + public NativeComponentRenderer Renderer { get; } + public CPF.UIElement Element { get; } + public object TargetElement => Element; + + public virtual void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName) + { + //switch (attributeName) + //{ + // case nameof(XF.Element.AutomationId): + // ElementControl.AutomationId = (string)attributeValue; + // break; + // case nameof(XF.Element.ClassId): + // ElementControl.ClassId = (string)attributeValue; + // break; + // case nameof(XF.Element.StyleId): + // ElementControl.StyleId = (string)attributeValue; + // break; + // default: + // if (!TryRegisterEvent(attributeName, attributeEventHandlerId)) + // { + // throw new NotImplementedException($"{GetType().FullName} doesn't recognize attribute '{attributeName}'"); + // } + // break; + //} + var p = Element.GetPropertyMetadata(attributeName); + if (p != null) + { + Element.SetValue(attributeValue.ConvertTo(p.PropertyType), attributeName); + } + } + + //private bool TryRegisterEvent(string eventName, ulong eventHandlerId) + //{ + // if (RegisteredEvents.TryGetValue(eventName, out var eventRegistration)) + // { + // Renderer.RegisterEvent(eventHandlerId, eventRegistration.ClearId); + // eventRegistration.SetId(eventHandlerId); + + // return true; + // } + // return false; + //} + } +} diff --git a/CPF.Razor/ICpfElementHandler.cs b/CPF.Razor/ICpfElementHandler.cs new file mode 100644 index 0000000..403728f --- /dev/null +++ b/CPF.Razor/ICpfElementHandler.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//using Microsoft.MobileBlazorBindings.Core; + +namespace CPF.Razor +{ + public interface ICpfElementHandler : IElementHandler + { + UIElement Element { get; } + } +} diff --git a/ConsoleApp1.sln b/ConsoleApp1.sln index 1901bb3..f0d8f66 100644 --- a/ConsoleApp1.sln +++ b/ConsoleApp1.sln @@ -59,6 +59,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CPF.Demo", "CPF_Demo\CPF.De EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "蓝图重制版", "蓝图重制版\蓝图重制版.csproj", "{003E155A-8C40-41AF-A796-ED17E729E013}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CPF.Razor", "CPF.Razor\CPF.Razor.csproj", "{87E1ED0A-BFBF-4F5E-9FDF-5EAFE48DD719}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CpfRazorSample", "CpfRazorSample\CpfRazorSample.csproj", "{25A4EE47-F5BD-4F1E-B143-3E3B50C5AC2A}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Private\SharedVSIX\SharedVSIX.projitems*{db53e8d7-dfb6-48eb-a7b6-d1cf762acb9b}*SharedItemsImports = 4 @@ -224,6 +228,18 @@ Global {003E155A-8C40-41AF-A796-ED17E729E013}.Release|Any CPU.Build.0 = Release|Any CPU {003E155A-8C40-41AF-A796-ED17E729E013}.类库d|Any CPU.ActiveCfg = 类库d|Any CPU {003E155A-8C40-41AF-A796-ED17E729E013}.类库d|Any CPU.Build.0 = 类库d|Any CPU + {87E1ED0A-BFBF-4F5E-9FDF-5EAFE48DD719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87E1ED0A-BFBF-4F5E-9FDF-5EAFE48DD719}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87E1ED0A-BFBF-4F5E-9FDF-5EAFE48DD719}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87E1ED0A-BFBF-4F5E-9FDF-5EAFE48DD719}.Release|Any CPU.Build.0 = Release|Any CPU + {87E1ED0A-BFBF-4F5E-9FDF-5EAFE48DD719}.类库d|Any CPU.ActiveCfg = Debug|Any CPU + {87E1ED0A-BFBF-4F5E-9FDF-5EAFE48DD719}.类库d|Any CPU.Build.0 = Debug|Any CPU + {25A4EE47-F5BD-4F1E-B143-3E3B50C5AC2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25A4EE47-F5BD-4F1E-B143-3E3B50C5AC2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25A4EE47-F5BD-4F1E-B143-3E3B50C5AC2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25A4EE47-F5BD-4F1E-B143-3E3B50C5AC2A}.Release|Any CPU.Build.0 = Release|Any CPU + {25A4EE47-F5BD-4F1E-B143-3E3B50C5AC2A}.类库d|Any CPU.ActiveCfg = Debug|Any CPU + {25A4EE47-F5BD-4F1E-B143-3E3B50C5AC2A}.类库d|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -241,4 +257,7 @@ Global {F34CFFEE-546F-490E-A76A-2792840B284D} = {2B729C46-7592-425A-87E9-D769A94881F7} {DF526631-D060-47F2-AFD4-62C6CEA2FE9A} = {2B729C46-7592-425A-87E9-D769A94881F7} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {16FB883C-167C-4E1A-B311-6D74452A3CD6} + EndGlobalSection EndGlobal diff --git a/CpfRazorSample/CpfRazorSample.csproj b/CpfRazorSample/CpfRazorSample.csproj new file mode 100644 index 0000000..f7ea705 --- /dev/null +++ b/CpfRazorSample/CpfRazorSample.csproj @@ -0,0 +1,35 @@ + + + + WinExe + netcoreapp3.0 + + + 9.0 + 3.0 + + + + true + + AnyCPU + + + + + + + + + + + + + + + + + + + + diff --git a/CpfRazorSample/Program.cs b/CpfRazorSample/Program.cs new file mode 100644 index 0000000..5786464 --- /dev/null +++ b/CpfRazorSample/Program.cs @@ -0,0 +1,34 @@ +using CPF.Platform; +using CPF.Skia; +using CPF.Windows; +using Microsoft.Extensions.Hosting; +using System; +using CPF.Razor; + +namespace CpfRazorSample +{ + class Program + { + [STAThread] + static void Main(string[] args) + { + Application.Initialize( + (OperatingSystemType.Windows, new WindowsPlatform(), new SkiaDrawingFactory()) + , (OperatingSystemType.OSX, new CPF.Mac.MacPlatform(), new SkiaDrawingFactory())//如果需要支持Mac才需要 + , (OperatingSystemType.Linux, new CPF.Linux.LinuxPlatform(), new SkiaDrawingFactory())//如果需要支持Linux才需要 + ); + + var host = Host.CreateDefaultBuilder() + .ConfigureServices((hostContext, services) => + { + // Register app-specific services + //services.AddSingleton(); + }) + .Build(); + + var window = new CPF.Controls.Window(); + host.AddComponent(window); + Application.Run(window); + } + } +} diff --git a/CpfRazorSample/Stylesheet1.css b/CpfRazorSample/Stylesheet1.css new file mode 100644 index 0000000..d895ab6 --- /dev/null +++ b/CpfRazorSample/Stylesheet1.css @@ -0,0 +1,502 @@ +/*@font-face { + font-family: '微软雅黑'; + src: url('res://ConsoleApp1/msyh.ttc'); +}*/ /*加载字体*/ + +/** { + FontFamily: 微软雅黑; +}*/ + +@media windows { + * { + FontFamily: '微软雅黑'; /*不同系统的字体不同,自己根据情况改或者使用内嵌字体*/ + } +} + +@media osx { + * { + FontFamily: '苹方-简'; + } +} + +@media linux { + * { + FontFamily: '文泉驿正黑'; + } +} +/*设置窗体标题栏背景颜色圆角*/ +/*#caption { + IsAntiAlias: true; + Background: #2c2c2c; + CornerRadius:5,5,0,0; + Height:30; +} +#frame { + CornerRadius: 5; + IsAntiAlias: true; +}*/ +Button { + BorderFill: #DCDFE6; + IsAntiAlias: True; + CornerRadius: 4,4,4,4; + Background: #FFFFFF; +} + + Button[IsMouseOver=true] { + BorderFill: rgb(198,226,255); + Background: rgb(236,245,255); + Foreground: rgb(64,158,255); + } + + Button[IsPressed=true] { + BorderFill: rgb(58,142,230); + } + + Button.primary { + BorderFill: rgb(64,158,255); + CornerRadius: 4,4,4,4; + Background: rgb(64,158,255); + Foreground: #FFFFFF; + } + + Button.primary[IsMouseOver=true] { + BorderFill: rgb(102,177,255); + Background: rgb(102,177,255); + Foreground: #FFFFFF; + } + + Button.primary[IsPressed=true] { + BorderFill: rgb(58,142,230); + Background: rgb(58,142,230); + } + + Button.success { + BorderFill: rgb(103,194,58); + CornerRadius: 4,4,4,4; + Background: rgb(103,194,58); + Foreground: #FFFFFF; + } + + Button.success[IsMouseOver=true] { + BorderFill: rgb(133,206,97); + Background: rgb(133,206,97); + Foreground: #FFFFFF; + } + + Button.success[IsPressed=true] { + BorderFill: rgb(93,175,52); + Background: rgb(93,175,52); + } + + Button.danger { + BorderFill: rgb(245,108,108); + Background: rgb(245,108,108); + CornerRadius: 4,4,4,4; + Foreground: #FFFFFF; + } + + Button.danger[IsMouseOver=true] { + BorderFill: rgb(247,137,137); + Background: rgb(247,137,137); + Foreground: #FFFFFF; + } + + Button.danger[IsPressed=true] { + BorderFill: rgb(221,97,97); + Background: rgb(221,97,97); + } + +#dialogClose { + BorderFill: null; +} + +TextBox, .textBox, DatePicker { + Background: #fff; + IsAntiAlias: true; + BorderFill: #DCDFE6; + CornerRadius: 4,4,4,4; + BorderStroke: 1; +} + + .groupPanel TextBox, DatePicker TextBox, .textBox TextBox { + BorderStroke: 0; + } + + .textBox[IsKeyboardFocusWithin=true] { + BorderFill: #1E9FFF; + } + +.singleLine { /*单行文本框*/ + AcceptsReturn: false; + HScrollBarVisibility: Hidden; + VScrollBarVisibility: Hidden; +} + + .singleLine #contentPresenter { + Padding: 3; + } + +.multiline { /*多行文本框*/ +} + + +.slotLeft { + CornerRadius: 0,4,4,0; + BorderFill: #DCDFE6; + Background: #F5F7FA; + BorderThickness: 1,0,0,0; + BorderType: BorderThickness; + MarginTop: 0; + MarginBottom: 0; + MarginRight: 0; +} + +RadioButton #radioButtonBorder { + StrokeFill: rgb(220,223,230); +} + +RadioButton[IsChecked=true] #radioButtonBorder { + StrokeFill: #1E9FFF; + Fill: #1E9FFF; +} + +RadioButton #optionMark { + StrokeFill: #1E9FFF; + Fill: #fff; +} + +CheckBox #indeterminateMark { + Fill: #1E9FFF; +} + +CheckBox #checkBoxBorder { + Background: #fff; + BorderFill: rgb(220,223,230); +} + +CheckBox[IsChecked=true] #checkBoxBorder { + Background: #1E9FFF; + BorderFill: #1E9FFF; +} + +CheckBox Polyline { + StrokeFill: #fff; +} + +.radioGroup { + BorderType: BorderThickness; + BorderFill: rgb(220,223,230); + BorderThickness: 1,1,0,1; +} + + .radioGroup RadioButton { + BorderType: BorderThickness; + BorderFill: rgb(220,223,230); + BorderThickness: 0,0,1,0; + } + + .radioGroup RadioButton #markPanel { + Visibility: Collapsed; + } + + .radioGroup RadioButton TextBlock { + Margin: 5; + } + + .radioGroup RadioButton[IsChecked=true] { + Background: rgb(64,158,255); + Foreground: #fff; + } + +.error { + Foreground: #f00; + Visibility: Collapsed; +} + + .error[DesignMode=true] { + Visibility: Visible; + } + +.twoLine[AttachedExtenstions.IsError=true] .error { + Visibility: Visible; +} + +.twoLine[AttachedExtenstions.IsError=true] .textBox { + BorderFill: #f00; +} + +ScrollBar { + Background: null; +} + + ScrollBar Thumb { + IsAntiAlias: true; + CornerRadius: 5; + } + + ScrollBar[Orientation=Horizontal] { + Height: 12; + } + + ScrollBar[Orientation=Vertical] { + Width: 12; + } + + ScrollBar #PART_LineUpButton, ScrollBar #PART_LineDownButton { + Visibility: Collapsed; + } + +ComboBox { + Background: #fff; + IsAntiAlias: true; + BorderFill: #DCDFE6; + CornerRadius: 4,4,4,4; + BorderStroke: 1; +} + + ComboBox[IsKeyboardFocusWithin=true] { + BorderFill: #1E9FFF; + } + +#DropDownPanel TextBlock { + MarginLeft: 5; + MarginTop: 2; + MarginBottom: 2; +} + +#dropDownBorder { + ShadowBlur: 2; + ShadowColor: rgba(0, 0, 0, 0.4); + BorderStroke: 0; +} + +#DropDownPanel[IsMouseOver=false] ScrollBar { + Visibility: Collapsed; +} + +#DropDownPanel ScrollBar[Orientation=Horizontal] { + Height: 10; +} + +#DropDownPanel ScrollBar[Orientation=Vertical] { + Width: 10; +} + +Slider { + IsAntiAlias: true; +} + + Slider Thumb { + IsAntiAlias: true; + Width: 16; + Height: 16; + CornerRadius: 7; + BorderFill: rgb(64,158,255); + BorderStroke: 2; + Background: #fff; + ZIndex: 1; + } + + + Slider Thumb[IsMouseOver=true] { + animation-name: sliderMouseOver; + animation-duration: 0.1s; + animation-iteration-count: 1; + animation-fill-mode: forwards; + } + + Slider #TrackBackground { + CornerRadius: 2; + Background: rgb(228,231,237); + BorderStroke: 0; + } + + Slider #decreaseRepeatButton { + Background: rgb(64,158,255); + CornerRadius: 2; + } + + Slider[Orientation=Horizontal] #decreaseRepeatButton { + Height: 4; + MarginLeft: 5; + } + + Slider[Orientation=Vertical] #decreaseRepeatButton { + Width: 4; + MarginBottom: 5; + } + +ProgressBar { + CornerRadius: 5; + IsAntiAlias: true; + BorderFill: null; + Background: rgb(235,238,245); +} + + ProgressBar #Indicator, ProgressBar #Animation { + CornerRadius: 5; + } + +NumericUpDown { + Background: #fff; + IsAntiAlias: true; + BorderFill: #DCDFE6; + CornerRadius: 4,4,4,4; + BorderStroke: 1; +} + + NumericUpDown RepeatButton { + Width: 20; + Background: rgb(245,247,250); + } + + NumericUpDown #decreaseBtn { + CornerRadius: 4,0,0,4; + } + + NumericUpDown #increaseBtn { + CornerRadius: 0,4,4,0; + } + + NumericUpDown #textBoxBorder { + BorderFill: #DCDFE6; + } + + NumericUpDown TextBox { + BorderStroke: 0; + } + +.widget { + IsAntiAlias: true; + BorderFill: #DCDFE6; + CornerRadius: 4,4,4,4; + BorderStroke: 1; +} + +.widgetHead { + Background: linear-gradient(0 0,0 100%,#F7F7F7 0,#F0F0F0 1); + BorderType: BorderThickness; + BorderThickness: 0,0,0,1; + BorderFill: #DCDFE6; +} + +DataGrid { + Foreground: #7a7a7a; +} + +DataGridCellTemplate, DataGridRow, DataGrid, .DataGridCell { + BorderFill: rgb(235,238,245); +} + +DataGridRow { + Height: 36; +} + + DataGridRow[IsMouseOver=true] { + Background: rgb(245,247,250); + } + + DataGridRow[IsSelected=true] { + Background: rgb(245,247,250); + } + +DataGridColumnTemplate { + Height: 38; + FontSize: 15; + FontStyle: Bold; + Background: #fff; + BorderFill: rgb(235,238,245); +} + +TabControl #headBorder { + Background: #fff; +} + +TabItem > Border { + BorderThickness: 0,0,0,2; +} + +TabItem[IsSelected=true] > Border { + BorderFill: #1E9FFF; +} + +TabItem[IsSelected=true] { + Foreground: #1E9FFF; +} + +TabControl[TabStripPlacement=Left] TabItem, TabControl[TabStripPlacement=Right] TabItem { + Width: 100%; + BorderType: BorderThickness; + BorderThickness: 0,0,0,1; + BorderFill: #e8e8e8; +} + + TabControl[TabStripPlacement=Left] TabItem TextBlock { + MarginRight: 0; + } + + TabControl[TabStripPlacement=Left] TabItem > Border { + Width: 100%; + } + +TabControl[TabStripPlacement=Left] #headerPanel, TabControl[TabStripPlacement=Right] #headerPanel { + Width: 100; + Background: rgb(245,247,250); +} + +.closeBtn[IsMouseOver=true] { + Fill: #171717; +} + +.placeholder { + IsHitTestVisible: false; + Foreground: "192,196,204"; + Visibility: Collapsed; +} + +.textBox[AttachedExtenstions.IsEmpty=true] .placeholder { + Visibility: Visible; +} + +.loginBox TextBox, .loginBox .placeholder { + FontSize: 16; +} + +.loginBox CheckBox { + Foreground: #757575; +} + +.searchBox Button { + CornerRadius: 0,4,4,0, +} + +#MenuPop #menuPanel > Border, #MenuPop ContextMenu > Border { + Background: #fff; + ShadowColor: rgba(0, 0, 0, 0.4); +} + +#MenuPop MenuItem[IsMouseOver=true] { + Background: #DCDFE6; +} + +ListBoxItem { + Width: 100%; +} + +#dialogClose, #dialogClose[IsMouseOver=true] { + Background: null; + BorderFill: null; +} + + #dialogClose[IsPressed=true] { + Background: null; + BorderFill: null; + } + + #dialogClose Line { + StrokeFill: 218,218,218; + } + + #dialogClose[IsMouseOver=true] Line { + StrokeFill: #fff; + } diff --git a/CpfRazorSample/Test.razor b/CpfRazorSample/Test.razor new file mode 100644 index 0000000..9483d06 --- /dev/null +++ b/CpfRazorSample/Test.razor @@ -0,0 +1,7 @@ + + + + + +@**@ +@**@ \ No newline at end of file diff --git a/CpfRazorSample/Window1.cs b/CpfRazorSample/Window1.cs new file mode 100644 index 0000000..c668042 --- /dev/null +++ b/CpfRazorSample/Window1.cs @@ -0,0 +1,52 @@ +using CPF; +using CPF.Animation; +using CPF.Charts; +using CPF.Controls; +using CPF.Drawing; +using CPF.Shapes; +using CPF.Styling; +using CPF.Svg; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CpfRazorSample +{ + public class Window1 : Window + { + protected override void InitializeComponent() + { + LoadStyleFile("res://CpfRazorSample/Stylesheet1.css");//加载样式文件,文件需要设置为内嵌资源 + + Title = "标题"; + Width = 500; + Height = 400; + Background = null; + Children.Add(new WindowFrame(this, new Panel + { + Width = "100%", + Height = "100%", + Children = + { + new Button{ Content="按钮" } + } + })); + + if (!DesignMode)//设计模式下不执行,也可以用#if !DesignMode + { + + } + } + +#if !DesignMode //用户代码写到这里,设计器下不执行,防止设计器出错 + protected override void OnInitialized() + { + base.OnInitialized(); + + } + //用户代码 + +#endif + } +} diff --git a/CpfRazorSample/_Imports.razor b/CpfRazorSample/_Imports.razor new file mode 100644 index 0000000..afab265 --- /dev/null +++ b/CpfRazorSample/_Imports.razor @@ -0,0 +1,3 @@ +@using CPF.Razor.Controls +@using CpfRazorSample +@*@using CPF.Controls*@ \ No newline at end of file