From aa14c286a5e1888581e36b13282ac79d1bd41778 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Mon, 12 Aug 2019 10:18:18 -0700 Subject: [PATCH] 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) --- BlinForms.Framework/BlinFormsRenderer.cs | 29 +-- BlinForms.Framework/Blontrol.cs | 128 -------------- BlinForms.Framework/BlontrolAdapter.cs | 167 ++++++++++++++++++ BlinForms.Framework/Controls/Button.cs | 30 +--- .../Controls/FormsComponentBase.cs | 6 + BlinForms.Framework/Controls/Label.cs | 28 +-- BlinForms.Framework/Controls/Panel.cs | 55 ++++++ BlinForms.Framework/Controls/Timer.cs | 80 ++++----- BlinForms.Framework/IControlPropertyMapper.cs | 10 ++ .../ReflectionControlPropertyMapper.cs | 44 +++++ Sample/WinCounter.razor | 16 +- 11 files changed, 353 insertions(+), 240 deletions(-) delete mode 100644 BlinForms.Framework/Blontrol.cs create mode 100644 BlinForms.Framework/BlontrolAdapter.cs create mode 100644 BlinForms.Framework/Controls/Panel.cs create mode 100644 BlinForms.Framework/IControlPropertyMapper.cs create mode 100644 BlinForms.Framework/ReflectionControlPropertyMapper.cs diff --git a/BlinForms.Framework/BlinFormsRenderer.cs b/BlinForms.Framework/BlinFormsRenderer.cs index 37e7b98..4a35103 100644 --- a/BlinForms.Framework/BlinFormsRenderer.cs +++ b/BlinForms.Framework/BlinFormsRenderer.cs @@ -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 _componentIdToControl = new Dictionary(); // TODO: Map to Control + private Dictionary _componentIdToAdapter = new Dictionary(); 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; } } diff --git a/BlinForms.Framework/Blontrol.cs b/BlinForms.Framework/Blontrol.cs deleted file mode 100644 index 8407b94..0000000 --- a/BlinForms.Framework/Blontrol.cs +++ /dev/null @@ -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 _childControls = new List(); - - internal static Dictionary> KnownElements { get; } - = new Dictionary>(); - - public Blontrol(BlinFormsRenderer renderer) - { - _renderer = renderer; - } - - internal void ApplyEdits(ArrayBuilderSegment edits, ArrayRange 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; - } - } -} diff --git a/BlinForms.Framework/BlontrolAdapter.cs b/BlinForms.Framework/BlontrolAdapter.cs new file mode 100644 index 0000000..2b8c03e --- /dev/null +++ b/BlinForms.Framework/BlontrolAdapter.cs @@ -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 +{ + /// + /// Represents a "shadow" item that Blazor uses to map changes into the live WinForms control tree. + /// + public class BlontrolAdapter + { + internal static Dictionary> KnownElements { get; } + = new Dictionary>(); + + public BlontrolAdapter(BlinFormsRenderer renderer) + { + Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); + } + + public BlontrolAdapter Parent { get; set; } + public List Children { get; } = new List(); + + // 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; } + + /// + /// Used for debugging purposes. + /// + public string Name { get; set; } + + public override string ToString() + { + return $"Blontrol: Name={Name ?? ""}, Target={TargetControl?.GetType().Name ?? ""}, #Children={Children.Count}"; + } + + internal void ApplyEdits(ArrayBuilderSegment edits, ArrayRange 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 .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); + } + } +} diff --git a/BlinForms.Framework/Controls/Button.cs b/BlinForms.Framework/Controls/Button.cs index ec52ed1..f342dfb 100644 --- a/BlinForms.Framework/Controls/Button.cs +++ b/BlinForms.Framework/Controls/Button.cs @@ -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) diff --git a/BlinForms.Framework/Controls/FormsComponentBase.cs b/BlinForms.Framework/Controls/FormsComponentBase.cs index 32b6d08..e19326a 100644 --- a/BlinForms.Framework/Controls/FormsComponentBase.cs +++ b/BlinForms.Framework/Controls/FormsComponentBase.cs @@ -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); diff --git a/BlinForms.Framework/Controls/Label.cs b/BlinForms.Framework/Controls/Label.cs index fbee3d4..0fbd8fc 100644 --- a/BlinForms.Framework/Controls/Label.cs +++ b/BlinForms.Framework/Controls/Label.cs @@ -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) diff --git a/BlinForms.Framework/Controls/Panel.cs b/BlinForms.Framework/Controls/Panel.cs new file mode 100644 index 0000000..7a1a068 --- /dev/null +++ b/BlinForms.Framework/Controls/Panel.cs @@ -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; + } + } + } + } +} diff --git a/BlinForms.Framework/Controls/Timer.cs b/BlinForms.Framework/Controls/Timer.cs index d229ab7..dd34ad0 100644 --- a/BlinForms.Framework/Controls/Timer.cs +++ b/BlinForms.Framework/Controls/Timer.cs @@ -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; + // } + // } + // } + //} } diff --git a/BlinForms.Framework/IControlPropertyMapper.cs b/BlinForms.Framework/IControlPropertyMapper.cs new file mode 100644 index 0000000..f0b48e9 --- /dev/null +++ b/BlinForms.Framework/IControlPropertyMapper.cs @@ -0,0 +1,10 @@ +namespace BlinForms.Framework +{ + internal interface IControlPropertyMapper + { + void SetControlProperty(ulong attributeEventHandlerId, + string attributeName, + object attributeValue, + string attributeEventUpdatesAttributeName); + } +} diff --git a/BlinForms.Framework/ReflectionControlPropertyMapper.cs b/BlinForms.Framework/ReflectionControlPropertyMapper.cs new file mode 100644 index 0000000..926ccc9 --- /dev/null +++ b/BlinForms.Framework/ReflectionControlPropertyMapper.cs @@ -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?"); + } + } + } +} diff --git a/Sample/WinCounter.razor b/Sample/WinCounter.razor index df8d4fc..987f163 100644 --- a/Sample/WinCounter.razor +++ b/Sample/WinCounter.razor @@ -1,19 +1,23 @@ @using BlinForms.Framework.Controls +@using System.Drawing -