Introduction of BlontrolAdapters

- Removed container Blontrols and replaced with shadow tree of BlontrolAdapters
- Temporarily removed Timer component (it's not a Control, so it's tricky)
This commit is contained in:
Eilon Lipton 2019-08-12 10:18:18 -07:00
Родитель 03a07eb7b1
Коммит aa14c286a5
11 изменённых файлов: 353 добавлений и 240 удалений

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

@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
@ -12,7 +11,7 @@ namespace BlinForms.Framework
{
public class BlinFormsRenderer : Renderer
{
private Dictionary<int, Blontrol> _componentIdToControl = new Dictionary<int, Blontrol>(); // TODO: Map to Control
private Dictionary<int, BlontrolAdapter> _componentIdToAdapter = new Dictionary<int, BlontrolAdapter>();
public BlinFormsRenderer(IServiceProvider serviceProvider)
: base(serviceProvider, new LoggerFactory())
@ -28,12 +27,20 @@ namespace BlinForms.Framework
{
var component = InstantiateComponent(typeof(T));
var componentId = AssignRootComponentId(component);
var control = _componentIdToControl[componentId] =
new Blontrol(this)
var adapter = _componentIdToAdapter[componentId] =
new BlontrolAdapter(this)
{
Size = new Size(500, 500),
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()
{
Width = 500,
Height = 500,
},
};
RootForm.Controls.Add(control);
RootForm.Controls.Add(adapter.TargetControl);
return RenderRootComponentAsync(componentId);
}
@ -46,17 +53,17 @@ namespace BlinForms.Framework
{
foreach (var updatedComponent in renderBatch.UpdatedComponents.Array.Take(renderBatch.UpdatedComponents.Count))
{
var control = _componentIdToControl[updatedComponent.ComponentId];
control.ApplyEdits(updatedComponent.Edits, renderBatch.ReferenceFrames);
var adapter = _componentIdToAdapter[updatedComponent.ComponentId];
adapter.ApplyEdits(updatedComponent.Edits, renderBatch.ReferenceFrames);
}
return Task.CompletedTask;
}
internal Blontrol CreateControlForChildComponent(int componentId)
internal BlontrolAdapter CreateAdapterForChildComponent(int componentId)
{
var result = new Blontrol(this);
_componentIdToControl[componentId] = result;
var result = new BlontrolAdapter(this);
_componentIdToAdapter[componentId] = result;
return result;
}
}

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

@ -1,128 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Forms;
using BlinForms.Framework.Controls;
using BlinForms.Framework.Elements;
using Microsoft.AspNetCore.Components.RenderTree;
namespace BlinForms.Framework
{
public class Blontrol : Control
{
private readonly BlinFormsRenderer _renderer;
private readonly List<Control> _childControls = new List<Control>();
internal static Dictionary<string, Func<BlinFormsRenderer, IBlazorNativeControl>> KnownElements { get; }
= new Dictionary<string, Func<BlinFormsRenderer, IBlazorNativeControl>>();
public Blontrol(BlinFormsRenderer renderer)
{
_renderer = renderer;
}
internal void ApplyEdits(ArrayBuilderSegment<RenderTreeEdit> edits, ArrayRange<RenderTreeFrame> referenceFrames)
{
foreach (var edit in edits)
{
switch (edit.Type)
{
case RenderTreeEditType.PrependFrame:
ApplyPrependFrame(edit.SiblingIndex, referenceFrames.Array, edit.ReferenceFrameIndex);
break;
case RenderTreeEditType.SetAttribute:
ApplySetAttribute(edit.SiblingIndex, 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)
{
//Controls.RemoveAt(siblingIndex);
}
private void ApplySetAttribute(int siblingIndex, ref RenderTreeFrame attributeFrame)
{
var target = (IBlazorNativeControl)Controls[siblingIndex];
target.ApplyAttribute(ref attributeFrame);
}
private void ApplyPrependFrame(int siblingIndex, RenderTreeFrame[] frames, int frameIndex)
{
ref var frame = ref frames[frameIndex];
switch (frame.FrameType)
{
case RenderTreeFrameType.Element: // ... then this gets called to create the native component
var element = CreateNativeControlForElement(frames, frameIndex);
// 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);
}
break;
case RenderTreeFrameType.Component: // ... first a bunch of these get created for each "thing"
{
var childControl = _renderer.CreateControlForChildComponent(frame.ComponentId);
AddChildControl(siblingIndex, childControl);
break;
}
case RenderTreeFrameType.Markup:
if (!string.IsNullOrWhiteSpace(frame.MarkupContent))
{
throw new NotImplementedException("Nonempty markup: " + frame.MarkupContent);
}
break;
case RenderTreeFrameType.Text:
if (!string.IsNullOrWhiteSpace(frame.TextContent))
{
throw new NotImplementedException("Nonempty text: " + frame.TextContent);
}
break;
default:
throw new NotImplementedException($"Not supported frame type: {frame.FrameType}");
}
}
private void AddChildControl(int siblingIndex, Control childControl)
{
Controls.Add(childControl);
// TODO: _childControls is never read from... only added to...
if (siblingIndex < _childControls.Count)
{
_childControls.Insert(siblingIndex, childControl);
}
else
{
_childControls.Add(childControl);
}
}
private IBlazorNativeControl CreateNativeControlForElement(RenderTreeFrame[] frames, int frameIndex)
{
ref var frame = ref frames[frameIndex];
var elementName = frame.ElementName;
var nativeControl = KnownElements[elementName](_renderer);
foreach (var attribute in AttributeUtil.ElementAttributeFrames(frames, frameIndex))
{
var attributeCopy = attribute;
nativeControl.ApplyAttribute(ref attributeCopy);
}
return nativeControl;
}
}
}

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

@ -0,0 +1,167 @@
using BlinForms.Framework.Elements;
using Microsoft.AspNetCore.Components.RenderTree;
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace BlinForms.Framework
{
/// <summary>
/// Represents a "shadow" item that Blazor uses to map changes into the live WinForms control tree.
/// </summary>
public class BlontrolAdapter
{
internal static Dictionary<string, Func<BlinFormsRenderer, Control>> KnownElements { get; }
= new Dictionary<string, Func<BlinFormsRenderer, Control>>();
public BlontrolAdapter(BlinFormsRenderer renderer)
{
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? A component might have multiple WinForms controls created?
public Control TargetControl { get; set; }
public BlinFormsRenderer Renderer { get; private set; }
/// <summary>
/// Used for debugging purposes.
/// </summary>
public string Name { get; set; }
public override string ToString()
{
return $"Blontrol: Name={Name ?? "<?>"}, Target={TargetControl?.GetType().Name ?? "<?>"}, #Children={Children.Count}";
}
internal void ApplyEdits(ArrayBuilderSegment<RenderTreeEdit> edits, ArrayRange<RenderTreeFrame> referenceFrames)
{
foreach (var edit in edits)
{
switch (edit.Type)
{
case RenderTreeEditType.PrependFrame:
ApplyPrependFrame(edit.SiblingIndex, referenceFrames.Array, edit.ReferenceFrameIndex);
break;
case RenderTreeEditType.SetAttribute:
ApplySetAttribute(edit.SiblingIndex, 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)
{
//Controls.RemoveAt(siblingIndex);
}
private void ApplySetAttribute(int siblingIndex, ref RenderTreeFrame attributeFrame)
{
//var target = (IBlazorNativeControl)Controls[siblingIndex];
//target.ApplyAttribute(ref attributeFrame);
}
private void ApplyPrependFrame(int siblingIndex, RenderTreeFrame[] frames, int frameIndex)
{
ref var frame = ref frames[frameIndex];
switch (frame.FrameType)
{
case RenderTreeFrameType.Element:
{
// Elements represent Winforms native controls
var element = CreateNativeControlForElement(frames, frameIndex);
TargetControl = element;
// Add the new nataive control to the parent's child controls (the parent adapter is our
// container, so the parent adapter's control is our control's container.
// TODO: What about siblingIndex?
Parent.TargetControl.Controls.Add(TargetControl);
//// 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);
//}
break;
}
case RenderTreeFrameType.Component:
{
// Components are represented by BlontrolAdapters
var childAdapter = Renderer.CreateAdapterForChildComponent(frame.ComponentId);
childAdapter.Name = $"For: '{frame.Component.GetType().FullName}'";
AddChildAdapter(siblingIndex, childAdapter);
break;
}
case RenderTreeFrameType.Markup:
{
if (!string.IsNullOrWhiteSpace(frame.MarkupContent))
{
throw new NotImplementedException("Nonempty markup: " + frame.MarkupContent);
}
break;
}
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);
}
break;
}
default:
throw new NotImplementedException($"Not supported frame type: {frame.FrameType}");
}
}
private Control CreateNativeControlForElement(RenderTreeFrame[] frames, int frameIndex)
{
ref var frame = ref frames[frameIndex];
var elementName = frame.ElementName;
var nativeControl = KnownElements[elementName](Renderer);
foreach (var attribute in AttributeUtil.ElementAttributeFrames(frames, frameIndex))
{
// TODO: Do smarter property setting...? Not calling <NativeControl>.ApplyAttribute(...) right now. Should it?
var attributeCopy = attribute;
var mapper = GetControlPropertyMapper(nativeControl);
mapper.SetControlProperty(attributeCopy.AttributeEventHandlerId, attributeCopy.AttributeName, attributeCopy.AttributeValue, attributeCopy.AttributeEventUpdatesAttributeName);
}
return nativeControl;
}
private void AddChildAdapter(int siblingIndex, BlontrolAdapter childAdapter)
{
childAdapter.Parent = this;
if (siblingIndex < Children.Count)
{
Children.Insert(siblingIndex, childAdapter);
}
else
{
Children.Add(childAdapter);
}
}
private static IControlPropertyMapper GetControlPropertyMapper(Control control)
{
// TODO: Have control-specific ones, but also need a general one for custom controls? Or maybe not needed?
return new ReflectionControlPropertyMapper(control);
}
}
}

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

@ -1,6 +1,4 @@
using System;
using System.Drawing;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
namespace BlinForms.Framework.Controls
@ -9,7 +7,7 @@ namespace BlinForms.Framework.Controls
{
static Button()
{
Blontrol.KnownElements.Add(typeof(Button).FullName, renderer => new BlazorButton(renderer));
BlontrolAdapter.KnownElements.Add(typeof(Button).FullName, renderer => new BlazorButton(renderer));
}
[Parameter] public string Text { get; set; }
@ -36,30 +34,6 @@ namespace BlinForms.Framework.Controls
};
}
protected override void OnParentChanged(EventArgs e)
{
base.OnParentChanged(e);
if (Parent != null)
{
Parent.Location = Location;
Location = Point.Empty;
Parent.Size = Size;
}
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
if (Parent != null)
{
Parent.Location = Location;
Location = Point.Empty;
Parent.Size = Size;
}
}
public void ApplyAttribute(ref RenderTreeFrame attribute)
{
switch (attribute.AttributeName)

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

@ -20,15 +20,21 @@ namespace BlinForms.Framework.Controls
builder.AddAttribute(200, nameof(Left), Left);
builder.AddAttribute(300, nameof(Width), Width);
builder.AddAttribute(400, nameof(Height), Height);
RenderContents(builder);
builder.CloseElement();
}
protected abstract void RenderAttributes(RenderTreeBuilder builder);
protected virtual void RenderContents(RenderTreeBuilder builder)
{
}
public static void ApplyAttribute(Control control, ref RenderTreeFrame attributeFrame)
{
switch (attributeFrame.AttributeName)
{
// TODO: Fix not setting default values for types. Maybe use nullable types on components?
case nameof(Top):
if ((attributeFrame.AttributeValue as string) != "0")
control.Top = int.Parse((string)attributeFrame.AttributeValue);

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

@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using System;
using System.Drawing;
namespace BlinForms.Framework.Controls
{
@ -9,7 +7,7 @@ namespace BlinForms.Framework.Controls
{
static Label()
{
Blontrol.KnownElements.Add(typeof(Label).FullName, renderer => new BlazorLabel());
BlontrolAdapter.KnownElements.Add(typeof(Label).FullName, renderer => new BlazorLabel());
}
[Parameter] public string Text { get; set; }
@ -25,30 +23,6 @@ namespace BlinForms.Framework.Controls
{
}
protected override void OnParentChanged(EventArgs e)
{
base.OnParentChanged(e);
if (Parent != null)
{
Parent.Location = Location;
Location = Point.Empty;
Parent.Size = Size;
}
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
if (Parent != null)
{
Parent.Location = Location;
Location = Point.Empty;
Parent.Size = Size;
}
}
public void ApplyAttribute(ref RenderTreeFrame attribute)
{
switch (attribute.AttributeName)

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

@ -0,0 +1,55 @@
using System.Drawing;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
namespace BlinForms.Framework.Controls
{
public class Panel : FormsComponentBase
{
static Panel()
{
BlontrolAdapter.KnownElements.Add(typeof(Panel).FullName, renderer => new BlazorPanel(renderer));
}
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public bool Visible { get; set; }
[Parameter] public Color BackColor { get; set; }
protected override void RenderAttributes(RenderTreeBuilder builder)
{
builder.AddAttribute(1, nameof(Visible), Visible);
builder.AddAttribute(2, nameof(BackColor), BackColor.ToArgb());
}
protected override void RenderContents(RenderTreeBuilder builder)
{
builder.AddContent(999999, ChildContent);
}
class BlazorPanel : System.Windows.Forms.Panel, IBlazorNativeControl
{
public BlazorPanel(BlinFormsRenderer renderer)
{
}
public void ApplyAttribute(ref RenderTreeFrame attribute)
{
switch (attribute.AttributeName)
{
case nameof(Visible):
Visible = (bool)attribute.AttributeValue;
break;
case nameof(BackColor):
BackColor = Color.FromArgb(argb: (int)attribute.AttributeValue);
break;
//case "onclick":
// ClickEventHandlerId = attribute.AttributeEventHandlerId;
// break;
default:
FormsComponentBase.ApplyAttribute(this, ref attribute);
break;
}
}
}
}
}

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

@ -4,49 +4,49 @@ using System.Windows.Forms;
namespace BlinForms.Framework.Controls
{
public class Timer : FormsComponentBase
{
static Timer()
{
Blontrol.KnownElements.Add(typeof(Timer).FullName, renderer => new BlazorTimer(renderer));
}
//public class Timer : FormsComponentBase
//{
// static Timer()
// {
// BlontrolAdapter.KnownElements.Add(typeof(Timer).FullName, renderer => new BlazorTimer(renderer));
// }
[Parameter] public EventCallback OnTick { get; set; }
// [Parameter] public EventCallback OnTick { get; set; }
protected override void RenderAttributes(RenderTreeBuilder builder)
{
builder.AddAttribute(1, "ontick", OnTick);
}
// protected override void RenderAttributes(RenderTreeBuilder builder)
// {
// builder.AddAttribute(1, "ontick", OnTick);
// }
class BlazorTimer : System.Windows.Forms.Timer, IBlazorNativeControl
{
public ulong TickEventHandlerId { get; set; }
// class BlazorTimer : System.Windows.Forms.Timer, IBlazorNativeControl
// {
// public ulong TickEventHandlerId { get; set; }
public BlazorTimer(BlinFormsRenderer renderer)
{
Interval = 1000;
Tick += (s, e) =>
{
if (TickEventHandlerId != default)
{
renderer.DispatchEventAsync(TickEventHandlerId, null, new UIEventArgs());
}
};
Enabled = true;
}
// public BlazorTimer(BlinFormsRenderer renderer)
// {
// Interval = 1000;
// Tick += (s, e) =>
// {
// if (TickEventHandlerId != default)
// {
// renderer.DispatchEventAsync(TickEventHandlerId, null, new UIEventArgs());
// }
// };
// Enabled = true;
// }
public void ApplyAttribute(ref RenderTreeFrame attribute)
{
switch (attribute.AttributeName)
{
case "ontick":
TickEventHandlerId = attribute.AttributeEventHandlerId;
break;
default:
//FormsComponentBase.ApplyAttribute(this, ref attribute);
break;
}
}
}
}
// public void ApplyAttribute(ref RenderTreeFrame attribute)
// {
// switch (attribute.AttributeName)
// {
// case "ontick":
// TickEventHandlerId = attribute.AttributeEventHandlerId;
// break;
// default:
// //FormsComponentBase.ApplyAttribute(this, ref attribute);
// break;
// }
// }
// }
//}
}

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

@ -0,0 +1,10 @@
namespace BlinForms.Framework
{
internal interface IControlPropertyMapper
{
void SetControlProperty(ulong attributeEventHandlerId,
string attributeName,
object attributeValue,
string attributeEventUpdatesAttributeName);
}
}

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

@ -0,0 +1,44 @@
using System.Diagnostics;
using System.Windows.Forms;
namespace BlinForms.Framework
{
internal class ReflectionControlPropertyMapper : IControlPropertyMapper
{
private readonly Control _control;
public ReflectionControlPropertyMapper(Control control)
{
_control = control;
}
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);
if (propertyInfo != null)
{
if (propertyInfo.PropertyType == typeof(string))
{
propertyInfo.SetValue(_control, attributeValue);
}
else if (propertyInfo.PropertyType == typeof(int))
{
propertyInfo.SetValue(_control, int.Parse((string)attributeValue));
}
else if (propertyInfo.PropertyType == typeof(bool))
{
propertyInfo.SetValue(_control, attributeValue);
}
else
{
Debug.WriteLine($"Unknown property type '{propertyInfo.PropertyType}' for '{attributeName}' on '{_control.GetType().FullName}'.");
}
}
else
{
Debug.WriteLine($"Unknown property '{attributeName}' on '{_control.GetType().FullName}'. Maybe an event handler?");
}
}
}
}

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

@ -1,19 +1,23 @@
@using BlinForms.Framework.Controls
@using System.Drawing
<Button Top="50" Width="300" Text="@("Increment: " + count)" OnClick="@IncrementCount" />
<Button Top="50" Width="300" Text="@("Increment: " + count)" Height="20" OnClick="@IncrementCount" />
<Label Top="100" Width="200" Text="@("I'm a label: " + labelCount)"></Label>
<Timer OnTick="@OnTimerTick" />
<Panel Visible="true" BackColor="Color.Red" Top="300" Left="300" Width="300" Height="50">
</Panel>
<Button Top="200" Width="400" Text="@($"Toggle visible (visible={isDetailsVisible})")" OnClick="@ToggleVisible" />
<Label Top="100" Width="200" Height="20" Text="@("I'm a label: " + labelCount)"></Label>
@*<Timer OnTick="@OnTimerTick" />*@
<Button Top="200" Width="400" Height="20" Text="@($"Toggle visible (visible={isDetailsVisible})")" OnClick="@ToggleVisible" />
@if (isDetailsVisible)
{
<Label Top="250" Width="200" Text="I am visible!"></Label>
<Label Top="250" Width="200" Height="20" Text="I am visible!"></Label>
}
else
{
<Label Top="300" Width="200" Text="Not visible..."></Label>
<Label Top="300" Width="200" Height="20" Text="Not visible..."></Label>
}
@code {