Factor .NET Standard components of BlinForms into new Emblazon library

- Emblazon library is .NET Standard 2.0
- BlinForms remains .NET Core App 3.0 (required for WinForms)
- Made things reasonably generic in Emblazon to support future native renderers (e.g. Blaxamarin)
This commit is contained in:
Eilon Lipton 2019-08-20 13:48:15 -07:00
Родитель 84bdc4f5ca
Коммит bbb4bb1f5c
26 изменённых файлов: 454 добавлений и 345 удалений

Просмотреть файл

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>

Просмотреть файл

@ -3,12 +3,11 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.0.0-preview8.19372.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0-preview7.19362.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.0-preview7.19362.4" />
<ProjectReference Include="..\Emblazon\Emblazon.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -6,7 +6,7 @@ namespace BlinForms.Framework
{
public static class BlinForms
{
public static void Run<T>() where T: IComponent
public static void Run<T>() where T : IComponent
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

Просмотреть файл

@ -1,46 +1,29 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Logging;
using Emblazon;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace BlinForms.Framework
{
public class BlinFormsRenderer : Renderer
public class BlinFormsRenderer : EmblazonRenderer<Control>
{
private readonly Dictionary<int, BlontrolAdapter> _componentIdToAdapter = new Dictionary<int, BlontrolAdapter>();
public BlinFormsRenderer(IServiceProvider serviceProvider)
: base(serviceProvider, new LoggerFactory())
: base(serviceProvider)
{
}
public Form RootForm { get; private set; } = new RootForm();
public override Dispatcher Dispatcher { get; }
= Dispatcher.CreateDefault();
public Task AddComponent<T>() where T : IComponent
protected override void InitializeRootAdapter(EmblazonAdapter<Control> adapter)
{
var component = InstantiateComponent(typeof(T));
var componentId = AssignRootComponentId(component);
var adapter = _componentIdToAdapter[componentId] =
new BlontrolAdapter(this)
{
Name = "Root BlontrolAdapter",
// TODO: Might actually want to keep this dummy control so that Blinforms can be an island in a form. But, need
// to figure out its default size etc. Perhaps top-level Razor class implements ITopLevel{FormSettings} interface
// to control 'container Form' options?
TargetControl = new Control()
{
Dock = DockStyle.Fill,
},
};
// TODO: Might actually want to keep this dummy control so that Blinforms can be an island in a form. But, need
// to figure out its default size etc. Perhaps top-level Razor class implements ITopLevel{FormSettings} interface
// to control 'container Form' options?
adapter.TargetControl = new Control()
{
Dock = DockStyle.Fill,
};
RootForm.Controls.Add(adapter.TargetControl);
return RenderRootComponentAsync(componentId);
}
protected override void HandleException(Exception exception)
@ -48,22 +31,9 @@ namespace BlinForms.Framework
MessageBox.Show(exception.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
protected override EmblazonAdapter<Control> CreateAdapter()
{
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);
}
return Task.CompletedTask;
}
internal BlontrolAdapter CreateAdapterForChildComponent(int componentId)
{
var result = new BlontrolAdapter(this);
_componentIdToAdapter[componentId] = result;
return result;
return new BlontrolAdapter();
}
}
}

Просмотреть файл

@ -1,227 +1,34 @@
using BlinForms.Framework.Controls;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics;
using System.Windows.Forms;
using Emblazon;
namespace BlinForms.Framework
{
/// <summary>
/// Represents a "shadow" item that Blazor uses to map changes into the live WinForms control tree.
/// </summary>
public class BlontrolAdapter
public class BlontrolAdapter : EmblazonAdapter<Control>
{
internal static Dictionary<string, IComponentControlFactory> KnownElements { get; }
= new Dictionary<string, IComponentControlFactory>();
public BlontrolAdapter(BlinFormsRenderer renderer)
public BlontrolAdapter()
{
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
}
public BlontrolAdapter Parent { get; set; }
public List<BlontrolAdapter> Children { get; } = new List<BlontrolAdapter>();
// TODO: Is this the right concept? Can a component have multiple WinForms controls created?
public Control TargetControl { get; set; }
public BlinFormsRenderer Renderer { get; }
/// <summary>
/// Used for debugging purposes.
/// </summary>
public string Name { get; set; }
public override string ToString()
protected override void RemoveChildControl(EmblazonAdapter<Control> child)
{
return $"Bladapter: Name={Name ?? "<?>"}, Target={TargetControl?.GetType().Name ?? "<?>"}, #Children={Children.Count}";
TargetControl.Controls.Remove(child.TargetControl);
}
internal void ApplyEdits(int componentId, ArrayBuilderSegment<RenderTreeEdit> edits, ArrayRange<RenderTreeFrame> referenceFrames, RenderBatch batch)
protected override EmblazonAdapter<Control> CreateAdapter()
{
foreach (var edit in edits)
{
switch (edit.Type)
{
case RenderTreeEditType.PrependFrame:
ApplyPrependFrame(batch, componentId, edit.SiblingIndex, referenceFrames.Array, edit.ReferenceFrameIndex);
break;
case RenderTreeEditType.SetAttribute:
ApplySetAttribute(ref referenceFrames.Array[edit.ReferenceFrameIndex]);
break;
case RenderTreeEditType.RemoveFrame:
ApplyRemoveFrame(edit.SiblingIndex);
break;
default:
throw new NotImplementedException($"Not supported edit type: {edit.Type}");
}
}
return new BlontrolAdapter();
}
private void ApplyRemoveFrame(int siblingIndex)
protected override bool IsChildControlParented(Control nativeChild)
{
var childToRemove = Children[siblingIndex];
// If there's a target control for the child adapter, remove it from the live control tree.
// Not all adapters have target controls; Adapters for markup/text have no associated native control.
if (childToRemove.TargetControl != null)
{
TargetControl.Controls.Remove(childToRemove.TargetControl);
}
Children.RemoveAt(siblingIndex);
return nativeChild.Parent != null;
}
private void ApplySetAttribute(ref RenderTreeFrame attributeFrame)
{
var mapper = GetControlPropertyMapper(TargetControl);
mapper.SetControlProperty(attributeFrame.AttributeEventHandlerId, attributeFrame.AttributeName, attributeFrame.AttributeValue, attributeFrame.AttributeEventUpdatesAttributeName);
}
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 BlontrolAdapters
var childAdapter = Renderer.CreateAdapterForChildComponent(frame.ComponentId);
childAdapter.Name = $"For: '{frame.Component.GetType().FullName}'";
AddChildAdapter(siblingIndex, childAdapter);
return 1;
}
case RenderTreeFrameType.Region:
{
return InsertFrameRange(batch, componentId, siblingIndex, frames, frameIndex + 1, frameIndex + frame.RegionSubtreeLength);
}
case RenderTreeFrameType.Markup:
{
if (!string.IsNullOrWhiteSpace(frame.MarkupContent))
{
throw new NotImplementedException("Nonempty markup: " + frame.MarkupContent);
}
AddChildAdapter(siblingIndex, new BlontrolAdapter(Renderer) { Name = $"Dummy markup, sib#={siblingIndex}" });
return 1;
}
case RenderTreeFrameType.Text:
{
// TODO: Maybe support this for Labels for Text property, etc. ("DefaultProperty"?)
if (!string.IsNullOrWhiteSpace(frame.TextContent))
{
throw new NotImplementedException("Nonempty text: " + frame.TextContent);
}
AddChildAdapter(siblingIndex, new BlontrolAdapter(Renderer) { Name = $"Dummy text, sib#={siblingIndex}" });
return 1;
}
default:
throw new NotImplementedException($"Not supported frame type: {frame.FrameType}");
}
}
private void InsertElement(int siblingIndex, RenderTreeFrame[] frames, int frameIndex, int componentId, RenderBatch batch)
{
// Elements represent Winforms native controls
ref var frame = ref frames[frameIndex];
var elementName = frame.ElementName;
var controlFactory = KnownElements[elementName];
var nativeControl = controlFactory.CreateControl(new ComponentControlFactoryContext(Renderer, Parent?.TargetControl));
TargetControl = nativeControl;
// TODO: Need a more reliable way to know whether the target control is already created, e.g. a return value
// from ControlFactory.CreateControl(). Right now the check assumes that if the target control is already parented,
// there is no need to parent it. Not an awful assumption, but looks odd.
if (TargetControl.Parent == null)
{
// Add the new native control to the parent's child controls (the parent adapter is our
// container, so the parent adapter's control is our control's container.
AddChildControl(Parent.TargetControl, siblingIndex, TargetControl);
}
var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength;
for (var descendantIndex = frameIndex + 1; descendantIndex < endIndexExcl; descendantIndex++)
{
var candidateFrame = frames[descendantIndex];
if (candidateFrame.FrameType == RenderTreeFrameType.Attribute)
{
// TODO: Do smarter property setting...? Not calling <NativeControl>.ApplyAttribute(...) right now. Should it?
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;
}
}
//// Ignoring non-controls, such as Timer Component
//if (element is Control elementControl)
//{
// AddChildControl(siblingIndex, elementControl);
//}
//else
//{
// Debug.WriteLine("Ignoring non-control child: " + element.GetType().FullName);
//}
}
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 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, BlontrolAdapter 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);
}
}
private static void AddChildControl(Control parentControl, int siblingIndex, Control childControl)
protected override void AddChildControl(Control parentControl, int siblingIndex, Control childControl)
{
if (siblingIndex <= parentControl.Controls.Count)
{
@ -236,18 +43,5 @@ namespace BlinForms.Framework
parentControl.Controls.Add(childControl);
}
}
private static IControlPropertyMapper GetControlPropertyMapper(Control control)
{
// TODO: Have control-specific ones, but also need a general one for custom controls? Or maybe not needed?
if (control is IBlazorNativeControl nativeControl)
{
return new NativeControlPropertyMapper(nativeControl);
}
else
{
return new ReflectionControlPropertyMapper(control);
}
}
}
}

Просмотреть файл

@ -1,16 +0,0 @@
using System.Windows.Forms;
namespace BlinForms.Framework
{
public class ComponentControlFactoryContext
{
public ComponentControlFactoryContext(BlinFormsRenderer renderer, Control parentControl)
{
Renderer = renderer ?? throw new System.ArgumentNullException(nameof(renderer));
ParentControl = parentControl;
}
public Control ParentControl { get; }
public BlinFormsRenderer Renderer { get; }
}
}

Просмотреть файл

@ -1,20 +0,0 @@
using System;
using System.Windows.Forms;
namespace BlinForms.Framework
{
public class ComponentControlFactoryFunc : IComponentControlFactory
{
private readonly Func<BlinFormsRenderer, Control, Control> _callback;
public ComponentControlFactoryFunc(Func<BlinFormsRenderer, Control, Control> callback)
{
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
public Control CreateControl(ComponentControlFactoryContext context)
{
return _callback(context.Renderer, context.ParentControl);
}
}
}

Просмотреть файл

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Emblazon;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
namespace BlinForms.Framework.Controls
@ -7,7 +8,7 @@ namespace BlinForms.Framework.Controls
{
static Button()
{
BlontrolAdapter.KnownElements.Add(typeof(Button).FullName, new ComponentControlFactoryFunc((renderer, _) => new BlazorButton(renderer)));
BlontrolAdapter.KnownElements.Add(typeof(Button).FullName, new ComponentControlFactoryFunc<System.Windows.Forms.Control>((renderer, _) => new BlazorButton(renderer)));
}
[Parameter] public string Text { get; set; }
@ -24,7 +25,7 @@ namespace BlinForms.Framework.Controls
class BlazorButton : System.Windows.Forms.Button, IBlazorNativeControl
{
public BlazorButton(BlinFormsRenderer renderer)
public BlazorButton(EmblazonRenderer<System.Windows.Forms.Control> renderer)
{
Click += (s, e) =>
{

Просмотреть файл

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Emblazon;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
namespace BlinForms.Framework.Controls
@ -7,7 +8,7 @@ namespace BlinForms.Framework.Controls
{
static Label()
{
BlontrolAdapter.KnownElements.Add(typeof(Label).FullName, new ComponentControlFactoryFunc((_, __) => new BlazorLabel()));
BlontrolAdapter.KnownElements.Add(typeof(Label).FullName, new ComponentControlFactoryFunc<System.Windows.Forms.Control>((_, __) => new BlazorLabel()));
}
[Parameter] public string Text { get; set; }

Просмотреть файл

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Emblazon;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
namespace BlinForms.Framework.Controls
@ -7,7 +8,7 @@ namespace BlinForms.Framework.Controls
{
static Panel()
{
BlontrolAdapter.KnownElements.Add(typeof(Panel).FullName, new ComponentControlFactoryFunc((_, __) => new BlazorPanel()));
BlontrolAdapter.KnownElements.Add(typeof(Panel).FullName, new ComponentControlFactoryFunc<System.Windows.Forms.Control>((_, __) => new BlazorPanel()));
}
[Parameter] public RenderFragment ChildContent { get; set; }

Просмотреть файл

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Emblazon;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using System.Windows.Forms;
@ -8,7 +9,7 @@ namespace BlinForms.Framework.Controls
{
static SplitContainer()
{
BlontrolAdapter.KnownElements.Add(typeof(SplitContainer).FullName, new ComponentControlFactoryFunc((_, __) => new BlazorSplitContainer()));
BlontrolAdapter.KnownElements.Add(typeof(SplitContainer).FullName, new ComponentControlFactoryFunc<System.Windows.Forms.Control>((_, __) => new BlazorSplitContainer()));
}
[Parameter] public RenderFragment ChildContent { get; set; }

Просмотреть файл

@ -1,10 +1,12 @@
namespace BlinForms.Framework.Controls
using Emblazon;
namespace BlinForms.Framework.Controls
{
public class SplitterPanel1 : SplitterPanelBase
{
static SplitterPanel1()
{
BlontrolAdapter.KnownElements.Add(typeof(SplitterPanel1).FullName, new ComponentControlFactoryFunc((_, parentControl) => GetSplitterPanel(parentControl, panelNumber: 1)));
BlontrolAdapter.KnownElements.Add(typeof(SplitterPanel1).FullName, new ComponentControlFactoryFunc<System.Windows.Forms.Control>((_, parentControl) => GetSplitterPanel(parentControl, panelNumber: 1)));
}
}
}

Просмотреть файл

@ -1,10 +1,12 @@
namespace BlinForms.Framework.Controls
using Emblazon;
namespace BlinForms.Framework.Controls
{
public class SplitterPanel2 : SplitterPanelBase
{
static SplitterPanel2()
{
BlontrolAdapter.KnownElements.Add(typeof(SplitterPanel2).FullName, new ComponentControlFactoryFunc((_, parentControl) => GetSplitterPanel(parentControl, panelNumber: 2)));
BlontrolAdapter.KnownElements.Add(typeof(SplitterPanel2).FullName, new ComponentControlFactoryFunc<System.Windows.Forms.Control>((_, parentControl) => GetSplitterPanel(parentControl, panelNumber: 2)));
}
}
}

Просмотреть файл

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Emblazon;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using System.Windows.Forms;
@ -8,7 +9,7 @@ namespace BlinForms.Framework.Controls
{
static TextBox()
{
BlontrolAdapter.KnownElements.Add(typeof(TextBox).FullName, new ComponentControlFactoryFunc((renderer, _) => new BlazorTextBox(renderer)));
BlontrolAdapter.KnownElements.Add(typeof(TextBox).FullName, new ComponentControlFactoryFunc<System.Windows.Forms.Control>((renderer, _) => new BlazorTextBox(renderer)));
}
[Parameter] public string Text { get; set; }
@ -43,7 +44,7 @@ namespace BlinForms.Framework.Controls
class BlazorTextBox : System.Windows.Forms.TextBox, IBlazorNativeControl
{
public BlazorTextBox(BlinFormsRenderer renderer)
public BlazorTextBox(EmblazonRenderer<System.Windows.Forms.Control> renderer)
{
}

Просмотреть файл

@ -1,9 +0,0 @@
using System.Windows.Forms;
namespace BlinForms.Framework
{
public interface IComponentControlFactory
{
Control CreateControl(ComponentControlFactoryContext context);
}
}

Просмотреть файл

@ -16,7 +16,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlaxamarinSample.Android",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlaxamarinSample.iOS", "BlaxamarinSample\BlaxamarinSample.iOS\BlaxamarinSample.iOS.csproj", "{E966A851-1BEB-4381-B275-6F0D532CF268}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlaxamarinSample", "BlaxamarinSample\BlaxamarinSample\BlaxamarinSample.csproj", "{732D30E2-4E59-47E6-AF8A-35E2C5746D0D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlaxamarinSample", "BlaxamarinSample\BlaxamarinSample\BlaxamarinSample.csproj", "{732D30E2-4E59-47E6-AF8A-35E2C5746D0D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emblazon", "Emblazon\Emblazon.csproj", "{77F00237-2214-4B58-BF83-1D5CA6274545}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -92,6 +94,18 @@ Global
{732D30E2-4E59-47E6-AF8A-35E2C5746D0D}.Release|iPhone.Build.0 = Release|Any CPU
{732D30E2-4E59-47E6-AF8A-35E2C5746D0D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{732D30E2-4E59-47E6-AF8A-35E2C5746D0D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Debug|iPhone.Build.0 = Debug|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Release|Any CPU.Build.0 = Release|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Release|iPhone.ActiveCfg = Release|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Release|iPhone.Build.0 = Release|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{77F00237-2214-4B58-BF83-1D5CA6274545}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

Просмотреть файл

@ -0,0 +1,15 @@
namespace Emblazon
{
public class ComponentControlFactoryContext<TNativeComponent> where TNativeComponent : class
{
public ComponentControlFactoryContext(EmblazonRenderer<TNativeComponent> renderer, TNativeComponent parentControl)
{
Renderer = renderer ?? throw new System.ArgumentNullException(nameof(renderer));
ParentControl = parentControl;
}
public TNativeComponent ParentControl { get; }
public EmblazonRenderer<TNativeComponent> Renderer { get; }
}
}

Просмотреть файл

@ -0,0 +1,19 @@
using System;
namespace Emblazon
{
public class ComponentControlFactoryFunc<TNativeComponent> : IComponentControlFactory<TNativeComponent> where TNativeComponent : class
{
private readonly Func<EmblazonRenderer<TNativeComponent>, TNativeComponent, TNativeComponent> _callback;
public ComponentControlFactoryFunc(Func<EmblazonRenderer<TNativeComponent>, TNativeComponent, TNativeComponent> callback)
{
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
public TNativeComponent CreateControl(ComponentControlFactoryContext<TNativeComponent> context)
{
return _callback(context.Renderer, context.ParentControl);
}
}
}

13
Emblazon/Emblazon.csproj Normal file
Просмотреть файл

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.0.0-preview8.19372.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0-preview7.19362.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.0-preview7.19362.4" />
</ItemGroup>
</Project>

256
Emblazon/EmblazonAdapter.cs Normal file
Просмотреть файл

@ -0,0 +1,256 @@
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Emblazon
{
/// <summary>
/// Represents a "shadow" item that Blazor uses to map changes into the live WinForms control tree.
/// </summary>
public abstract class EmblazonAdapter<TNativeComponent> where TNativeComponent : class
{
// TODO: This used to be internal, but now it's public so that BlinForms components can register themselves. How should this work?
public static Dictionary<string, IComponentControlFactory<TNativeComponent>> KnownElements { get; }
= new Dictionary<string, IComponentControlFactory<TNativeComponent>>();
public EmblazonAdapter()
{
}
public EmblazonAdapter<TNativeComponent> Parent { get; set; }
public List<EmblazonAdapter<TNativeComponent>> Children { get; } = new List<EmblazonAdapter<TNativeComponent>>();
// TODO: Is this the right concept? Can a component have multiple WinForms controls created?
public TNativeComponent TargetControl { get; set; }
public EmblazonRenderer<TNativeComponent> Renderer { get; private set; }
internal void SetRenderer(EmblazonRenderer<TNativeComponent> renderer)
{
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
}
/// <summary>
/// Used for debugging purposes.
/// </summary>
public string Name { get; set; }
public override string ToString()
{
return $"EmblazonAdapter: Name={Name ?? "<?>"}, Target={TargetControl?.GetType().Name ?? "<?>"}, #Children={Children.Count}";
}
internal void ApplyEdits(int componentId, ArrayBuilderSegment<RenderTreeEdit> edits, ArrayRange<RenderTreeFrame> referenceFrames, RenderBatch batch)
{
foreach (var edit in edits)
{
switch (edit.Type)
{
case RenderTreeEditType.PrependFrame:
ApplyPrependFrame(batch, componentId, edit.SiblingIndex, referenceFrames.Array, edit.ReferenceFrameIndex);
break;
case RenderTreeEditType.SetAttribute:
ApplySetAttribute(ref referenceFrames.Array[edit.ReferenceFrameIndex]);
break;
case RenderTreeEditType.RemoveFrame:
ApplyRemoveFrame(edit.SiblingIndex);
break;
default:
throw new NotImplementedException($"Not supported edit type: {edit.Type}");
}
}
}
private void ApplyRemoveFrame(int siblingIndex)
{
var childToRemove = Children[siblingIndex];
// If there's a target control for the child adapter, remove it from the live control tree.
// Not all adapters have target controls; Adapters for markup/text have no associated native control.
if (childToRemove.TargetControl != null)
{
RemoveChildControl(childToRemove);
}
Children.RemoveAt(siblingIndex);
}
protected abstract void RemoveChildControl(EmblazonAdapter<TNativeComponent> child);
private void ApplySetAttribute(ref RenderTreeFrame attributeFrame)
{
var mapper = GetControlPropertyMapper(TargetControl);
mapper.SetControlProperty(attributeFrame.AttributeEventHandlerId, attributeFrame.AttributeName, attributeFrame.AttributeValue, attributeFrame.AttributeEventUpdatesAttributeName);
}
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 BlontrolAdapters
var childAdapter = Renderer.CreateAdapterForChildComponent(frame.ComponentId);
childAdapter.Name = $"For: '{frame.Component.GetType().FullName}'";
AddChildAdapter(siblingIndex, childAdapter);
return 1;
}
case RenderTreeFrameType.Region:
{
return InsertFrameRange(batch, componentId, siblingIndex, frames, frameIndex + 1, frameIndex + frame.RegionSubtreeLength);
}
case RenderTreeFrameType.Markup:
{
if (!string.IsNullOrWhiteSpace(frame.MarkupContent))
{
throw new NotImplementedException("Nonempty markup: " + frame.MarkupContent);
}
var childAdapter = CreateAdapter();
childAdapter.Name = $"Dummy markup, sib#={siblingIndex}";
childAdapter.SetRenderer(Renderer);
AddChildAdapter(siblingIndex, childAdapter);
return 1;
}
case RenderTreeFrameType.Text:
{
// TODO: Maybe support this for Labels for Text property, etc. ("DefaultProperty"?)
if (!string.IsNullOrWhiteSpace(frame.TextContent))
{
throw new NotImplementedException("Nonempty text: " + frame.TextContent);
}
var childAdapter = CreateAdapter();
childAdapter.Name = $"Dummy text, sib#={siblingIndex}";
childAdapter.SetRenderer(Renderer);
AddChildAdapter(siblingIndex, childAdapter);
return 1;
}
default:
throw new NotImplementedException($"Not supported frame type: {frame.FrameType}");
}
}
protected abstract EmblazonAdapter<TNativeComponent> CreateAdapter();
protected abstract bool IsChildControlParented(TNativeComponent nativeChild);
private void InsertElement(int siblingIndex, RenderTreeFrame[] frames, int frameIndex, int componentId, RenderBatch batch)
{
// Elements represent Winforms native controls
ref var frame = ref frames[frameIndex];
var elementName = frame.ElementName;
var controlFactory = KnownElements[elementName];
var nativeControl = controlFactory.CreateControl(new ComponentControlFactoryContext<TNativeComponent>(Renderer, Parent?.TargetControl));
TargetControl = nativeControl;
// TODO: Need a more reliable way to know whether the target control is already created, e.g. a return value
// from ControlFactory.CreateControl(). Right now the check assumes that if the target control is already parented,
// there is no need to parent it. Not an awful assumption, but looks odd.
if (!IsChildControlParented(TargetControl))
{
// Add the new native control to the parent's child controls (the parent adapter is our
// container, so the parent adapter's control is our control's container.
AddChildControl(Parent.TargetControl, siblingIndex, TargetControl);
}
var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength;
for (var descendantIndex = frameIndex + 1; descendantIndex < endIndexExcl; descendantIndex++)
{
var candidateFrame = frames[descendantIndex];
if (candidateFrame.FrameType == RenderTreeFrameType.Attribute)
{
// TODO: Do smarter property setting...? Not calling <NativeControl>.ApplyAttribute(...) right now. Should it?
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;
}
}
//// Ignoring non-controls, such as Timer Component
//if (element is Control elementControl)
//{
// AddChildControl(siblingIndex, elementControl);
//}
//else
//{
// Debug.WriteLine("Ignoring non-control child: " + element.GetType().FullName);
//}
}
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 int CountDescendantFrames(RenderTreeFrame frame)
{
switch (frame.FrameType)
{
// 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.
case RenderTreeFrameType.Component:
return frame.ComponentSubtreeLength - 1;
case RenderTreeFrameType.Element: return frame.ElementSubtreeLength - 1;
case RenderTreeFrameType.Region: return frame.RegionSubtreeLength - 1;
default:
return 0;
};
}
private void AddChildAdapter(int siblingIndex, EmblazonAdapter<TNativeComponent> 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);
}
}
protected abstract void AddChildControl(TNativeComponent parentControl, int siblingIndex, TNativeComponent childControl);
private static IControlPropertyMapper GetControlPropertyMapper(TNativeComponent control)
{
// TODO: Have control-specific ones, but also need a general one for custom controls? Or maybe not needed?
if (control is IBlazorNativeControl nativeControl)
{
return new NativeControlPropertyMapper(nativeControl);
}
else
{
return new ReflectionControlPropertyMapper(control);
}
}
}
}

Просмотреть файл

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Emblazon
{
public abstract class EmblazonRenderer<TNativeComponent> : Renderer where TNativeComponent : class
{
private readonly Dictionary<int, EmblazonAdapter<TNativeComponent>> _componentIdToAdapter = new Dictionary<int, EmblazonAdapter<TNativeComponent>>();
public EmblazonRenderer(IServiceProvider serviceProvider)
: base(serviceProvider, new LoggerFactory())
{
}
public override Dispatcher Dispatcher { get; }
= Dispatcher.CreateDefault();
public Task AddComponent<T>() where T : IComponent
{
var component = InstantiateComponent(typeof(T));
var componentId = AssignRootComponentId(component);
var adapter = CreateAdapter();
adapter.Name = "Root BlontrolAdapter";
adapter.SetRenderer(this);
InitializeRootAdapter(adapter);
_componentIdToAdapter[componentId] = adapter;
return RenderRootComponentAsync(componentId);
}
protected abstract void InitializeRootAdapter(EmblazonAdapter<TNativeComponent> adapter);
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);
}
return Task.CompletedTask;
}
internal EmblazonAdapter<TNativeComponent> CreateAdapterForChildComponent(int componentId)
{
var result = CreateAdapter();
result.SetRenderer(this);
_componentIdToAdapter[componentId] = result;
return result;
}
protected abstract EmblazonAdapter<TNativeComponent> CreateAdapter();
}
}

Просмотреть файл

@ -1,6 +1,6 @@
namespace BlinForms.Framework.Controls
namespace Emblazon
{
internal interface IBlazorNativeControl
public interface IBlazorNativeControl
{
void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName);
}

Просмотреть файл

@ -0,0 +1,7 @@
namespace Emblazon
{
public interface IComponentControlFactory<TNativeComponent> where TNativeComponent : class
{
TNativeComponent CreateControl(ComponentControlFactoryContext<TNativeComponent> context);
}
}

Просмотреть файл

@ -1,4 +1,4 @@
namespace BlinForms.Framework
namespace Emblazon
{
internal interface IControlPropertyMapper
{

Просмотреть файл

@ -1,6 +1,4 @@
using BlinForms.Framework.Controls;
namespace BlinForms.Framework
namespace Emblazon
{
internal class NativeControlPropertyMapper : IControlPropertyMapper
{

Просмотреть файл

@ -1,43 +1,42 @@
using System.Diagnostics;
using System.Windows.Forms;
namespace BlinForms.Framework
namespace Emblazon
{
internal class ReflectionControlPropertyMapper : IControlPropertyMapper
{
private readonly Control _control;
private readonly object _component;
public ReflectionControlPropertyMapper(Control control)
public ReflectionControlPropertyMapper(object component)
{
_control = control;
_component = component;
}
public void SetControlProperty(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName)
{
// TODO: Figure out how to wire up events, e.g. OnClick
var propertyInfo = _control.GetType().GetProperty(attributeName);
var propertyInfo = _component.GetType().GetProperty(attributeName);
if (propertyInfo != null)
{
if (propertyInfo.PropertyType == typeof(string))
{
propertyInfo.SetValue(_control, attributeValue);
propertyInfo.SetValue(_component, attributeValue);
}
else if (propertyInfo.PropertyType == typeof(int))
{
propertyInfo.SetValue(_control, int.Parse((string)attributeValue));
propertyInfo.SetValue(_component, int.Parse((string)attributeValue));
}
else if (propertyInfo.PropertyType == typeof(bool))
{
propertyInfo.SetValue(_control, attributeValue);
propertyInfo.SetValue(_component, attributeValue);
}
else
{
Debug.WriteLine($"Unknown property type '{propertyInfo.PropertyType}' for '{attributeName}' on '{_control.GetType().FullName}'.");
Debug.WriteLine($"Unknown property type '{propertyInfo.PropertyType}' for '{attributeName}' on '{_component.GetType().FullName}'.");
}
}
else
{
Debug.WriteLine($"Unknown property '{attributeName}' on '{_control.GetType().FullName}'. Maybe an event handler?");
Debug.WriteLine($"Unknown property '{attributeName}' on '{_component.GetType().FullName}'. Maybe an event handler?");
}
}
}