From f0a78d13bfcb64948b16142b8a9d16602eea4289 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 5 Jan 2018 14:31:06 +0000 Subject: [PATCH] Begin tracking association between components and DOM elements so that components can be refresh their own display --- samples/HostedInAspNet.Client/Program.cs | 4 +- .../src/Rendering/Renderer.ts | 24 +++++++- src/Microsoft.Blazor.Browser/DOM.cs | 42 +++++++++++++ .../DOMComponentRenderState.cs | 61 +++++++++++++++++++ src/Microsoft.Blazor.Browser/Renderer.cs | 22 ------- src/Microsoft.Blazor/Components/IComponent.cs | 4 +- test/testapps/BasicTestApp/Program.cs | 2 +- .../BasicTestApp/TextOnlyComponent.cs | 2 +- 8 files changed, 131 insertions(+), 30 deletions(-) create mode 100644 src/Microsoft.Blazor.Browser/DOM.cs create mode 100644 src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs delete mode 100644 src/Microsoft.Blazor.Browser/Renderer.cs diff --git a/samples/HostedInAspNet.Client/Program.cs b/samples/HostedInAspNet.Client/Program.cs index c972eccd7f..2274c518cb 100644 --- a/samples/HostedInAspNet.Client/Program.cs +++ b/samples/HostedInAspNet.Client/Program.cs @@ -13,13 +13,13 @@ namespace HostedInAspNet.Client { // Temporarily render this test component until there's a proper mechanism // for testing this. - Renderer.Render(new MyComponent(), "app"); + DOM.AttachComponent("app", new MyComponent()); } } internal class MyComponent : IComponent { - public void Render(UITreeBuilder builder) + public void BuildUITree(UITreeBuilder builder) { builder.OpenElement("h1"); builder.AddText("Hello from UITree"); diff --git a/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts b/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts index 81d62cde0b..91415fd055 100644 --- a/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts +++ b/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts @@ -3,16 +3,36 @@ import { System_String, System_Array } from '../Platform/Platform'; import { platform } from '../Environment'; import { getTreeNodePtr, uiTreeNode, NodeType, UITreeNodePointer } from './UITreeNode'; +// TODO: Instead of associating components to parent elements, associate them with a +// start/end node, so that components don't have to be enclosed in a wrapper +// TODO: To avoid leaking memory, automatically remove entries from this dict as soon +// as the corresponding DOM nodes are removed (or maybe when the associated component +// is disposed, assuming we can guarantee that always happens). +const componentIdToParentElement: { [componentId: string]: Element } = {}; + +registerFunction('_blazorAttachComponentToElement', attachComponentToElement); registerFunction('_blazorRender', renderUITree); -function renderUITree(elementSelector: System_String, tree: System_Array, treeLength: number) { +function attachComponentToElement(elementSelector: System_String, componentId: System_String) { const elementSelectorJs = platform.toJavaScriptString(elementSelector); const element = document.querySelector(elementSelectorJs); if (!element) { - throw new Error(`Could not find any element matching selector '${ elementSelectorJs }'.`); + throw new Error(`Could not find any element matching selector '${elementSelectorJs}'.`); } clearElement(element); + + const componentIdJs = platform.toJavaScriptString(componentId); + componentIdToParentElement[componentIdJs] = element; +} + +function renderUITree(componentId: System_String, tree: System_Array, treeLength: number) { + const componentIdJs = platform.toJavaScriptString(componentId); + const element = componentIdToParentElement[componentIdJs]; + if (!element) { + throw new Error(`No element is currently associated with component ${componentIdJs}`); + } + insertNodeRange(element, tree, 0, treeLength - 1); } diff --git a/src/Microsoft.Blazor.Browser/DOM.cs b/src/Microsoft.Blazor.Browser/DOM.cs new file mode 100644 index 0000000000..c355928172 --- /dev/null +++ b/src/Microsoft.Blazor.Browser/DOM.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Blazor.Browser.Interop; +using Microsoft.Blazor.Components; +using Microsoft.Blazor.UITree; + +namespace Microsoft.Blazor.Browser +{ + /// + /// Provides mechanisms for displaying Blazor components in a browser Document + /// Object Model (DOM). + /// + public static class DOM + { + /// + /// Associates the specified component with the specified DOM element, causing the + /// component to be displayed there. + /// + /// A CSS selector that identifies a unique DOM element. + /// The component to be displayed in the DOM element. + public static void AttachComponent(string elementSelector, IComponent component) + { + var renderState = DOMComponentRenderState.GetOrCreate(component); + + RegisteredFunction.InvokeUnmarshalled( + "_blazorAttachComponentToElement", elementSelector, renderState.DOMComponentId); + + RefreshComponentInDOM(renderState); + } + + private static void RefreshComponentInDOM(DOMComponentRenderState renderState) + { + var tree = renderState.UpdateRender(); + RegisteredFunction.InvokeUnmarshalled( + "_blazorRender", + renderState.DOMComponentId, + tree.Array, + tree.Count); + } + } +} diff --git a/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs b/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs new file mode 100644 index 0000000000..970f2f2c1b --- /dev/null +++ b/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Blazor.Components; +using Microsoft.Blazor.UITree; +using System; +using System.Runtime.CompilerServices; + +namespace Microsoft.Blazor.Browser +{ + /// + /// Tracks the rendering state associated with an that is + /// being displayed in the DOM. + /// + internal class DOMComponentRenderState + { + private static ConditionalWeakTable _stateInstances + = new ConditionalWeakTable(); + private static long _nextDOMComponentId = 0; + + private readonly UITreeBuilder _uITreeBuilder; // TODO: Maintain two, so we can diff successive renders + + public string DOMComponentId { get; } + + public IComponent Component { get; } + + private DOMComponentRenderState(string componentId, IComponent component) + { + DOMComponentId = DOMComponentId; + Component = component; + _uITreeBuilder = new UITreeBuilder(); + } + + public static DOMComponentRenderState GetOrCreate(IComponent component) + { + lock (_stateInstances) + { + if (_stateInstances.TryGetValue(component, out var existingState)) + { + return existingState; + } + else + { + var newId = (_nextDOMComponentId++).ToString(); + var newState = new DOMComponentRenderState(newId, component); + _stateInstances.Add(component, newState); + return newState; + } + } + } + + public ArraySegment UpdateRender() + { + _uITreeBuilder.Clear(); + Component.BuildUITree(_uITreeBuilder); + + // TODO: Change this to return a diff between the previous render result and this new one + return _uITreeBuilder.GetNodes(); + } + } +} diff --git a/src/Microsoft.Blazor.Browser/Renderer.cs b/src/Microsoft.Blazor.Browser/Renderer.cs deleted file mode 100644 index b7871525bd..0000000000 --- a/src/Microsoft.Blazor.Browser/Renderer.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.Blazor.Browser.Interop; -using Microsoft.Blazor.Components; -using Microsoft.Blazor.UITree; - -namespace Microsoft.Blazor.Browser -{ - public static class Renderer - { - public static void Render(IComponent component, string elementSelector) - { - var builder = new UITreeBuilder(); - component.Render(builder); - - var tree = builder.GetNodes(); - RegisteredFunction.InvokeUnmarshalled( - "_blazorRender", elementSelector, tree.Array, tree.Count); - } - } -} diff --git a/src/Microsoft.Blazor/Components/IComponent.cs b/src/Microsoft.Blazor/Components/IComponent.cs index 95eef828d6..22c1cbbcc9 100644 --- a/src/Microsoft.Blazor/Components/IComponent.cs +++ b/src/Microsoft.Blazor/Components/IComponent.cs @@ -11,9 +11,9 @@ namespace Microsoft.Blazor.Components public interface IComponent { /// - /// Renders the component. + /// Builds a representing the current state of the component. /// /// A to which the rendered nodes should be appended. - void Render(UITreeBuilder builder); + void BuildUITree(UITreeBuilder builder); } } diff --git a/test/testapps/BasicTestApp/Program.cs b/test/testapps/BasicTestApp/Program.cs index 19a4ea56df..623ef67260 100644 --- a/test/testapps/BasicTestApp/Program.cs +++ b/test/testapps/BasicTestApp/Program.cs @@ -20,7 +20,7 @@ namespace BasicTestApp { var componentType = Type.GetType(componentTypeName); var componentInstance = (IComponent)Activator.CreateInstance(componentType); - Renderer.Render(componentInstance, "app"); + DOM.AttachComponent("app", componentInstance); } } } diff --git a/test/testapps/BasicTestApp/TextOnlyComponent.cs b/test/testapps/BasicTestApp/TextOnlyComponent.cs index 70b1108c63..9fd64cc941 100644 --- a/test/testapps/BasicTestApp/TextOnlyComponent.cs +++ b/test/testapps/BasicTestApp/TextOnlyComponent.cs @@ -8,7 +8,7 @@ namespace BasicTestApp { public class TextOnlyComponent : IComponent { - public void Render(UITreeBuilder builder) + public void BuildUITree(UITreeBuilder builder) { builder.AddText($"Hello from {nameof(TextOnlyComponent)}"); }