diff --git a/samples/HostedInAspNet.Client/Program.cs b/samples/HostedInAspNet.Client/Program.cs
index 3b148be37a..c5ae7fe9b2 100644
--- a/samples/HostedInAspNet.Client/Program.cs
+++ b/samples/HostedInAspNet.Client/Program.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Blazor.Browser;
+using Microsoft.Blazor.Browser.Rendering;
using Microsoft.Blazor.Components;
using Microsoft.Blazor.RenderTree;
@@ -13,7 +14,7 @@ namespace HostedInAspNet.Client
{
// Temporarily render this test component until there's a proper mechanism
// for testing this.
- DOM.AttachComponent("app", new MyComponent());
+ new BrowserRenderer().AddComponent("app", new MyComponent());
}
}
diff --git a/src/Microsoft.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts b/src/Microsoft.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
index 428d88a59c..90bb87b8ed 100644
--- a/src/Microsoft.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
+++ b/src/Microsoft.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
@@ -53,6 +53,11 @@ export const monoPlatform: Platform = {
},
callMethod: function callMethod(method: MethodHandle, target: System_Object, args: System_Object[]): System_Object {
+ if (args.length > 4) {
+ // Hopefully this restriction can be eased soon, but for now make it clear what's going on
+ throw new Error(`Currently, MonoPlatform supports passing a maximum of 4 arguments from JS to .NET. You tried to pass ${args.length}.`);
+ }
+
const stack = Module.Runtime.stackSave();
try {
diff --git a/src/Microsoft.Blazor.Browser.JS/src/Rendering/RenderTreeNode.ts b/src/Microsoft.Blazor.Browser.JS/src/Rendering/RenderTreeNode.ts
index 377954ed11..b6375efb77 100644
--- a/src/Microsoft.Blazor.Browser.JS/src/Rendering/RenderTreeNode.ts
+++ b/src/Microsoft.Blazor.Browser.JS/src/Rendering/RenderTreeNode.ts
@@ -1,6 +1,6 @@
import { System_String, System_Array, Pointer } from '../Platform/Platform';
import { platform } from '../Environment';
-const renderTreeNodeStructLength = 32;
+const renderTreeNodeStructLength = 36;
// To minimise GC pressure, instead of instantiating a JS object to represent each tree node,
// we work in terms of pointers to the structs on the .NET heap, and use static functions that
@@ -18,6 +18,9 @@ export const renderTreeNode = {
textContent: (node: RenderTreeNodePointer) => _readStringProperty(node, 12),
attributeName: (node: RenderTreeNodePointer) => _readStringProperty(node, 16),
attributeValue: (node: RenderTreeNodePointer) => _readStringProperty(node, 20),
+ attributeEventHandlerValue: (node: RenderTreeNodePointer) => _readObjectProperty(node, 24),
+ componentId: (node: RenderTreeNodePointer) => _readInt32Property(node, 28),
+ component: (node: RenderTreeNodePointer) => _readObjectProperty(node, 32),
};
export enum NodeType {
@@ -32,6 +35,10 @@ function _readInt32Property(baseAddress: Pointer, offsetBytes: number) {
return platform.readHeapInt32(baseAddress, offsetBytes);
}
+function _readObjectProperty(baseAddress: Pointer, offsetBytes: number) {
+ return platform.readHeapObject(baseAddress, offsetBytes);
+}
+
function _readStringProperty(baseAddress: Pointer, offsetBytes: number) {
var managedString = platform.readHeapObject(baseAddress, offsetBytes) as System_String;
return platform.toJavaScriptString(managedString);
diff --git a/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts b/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts
index 048ba3f7a5..fa4b1fa9e1 100644
--- a/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts
+++ b/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts
@@ -1,46 +1,57 @@
import { registerFunction } from '../RegisteredFunction';
-import { System_Object, System_String, System_Array, MethodHandle } from '../Platform/Platform';
+import { System_Object, System_String, System_Array, MethodHandle, Pointer } from '../Platform/Platform';
import { platform } from '../Environment';
import { getTreeNodePtr, renderTreeNode, NodeType, RenderTreeNodePointer } from './RenderTreeNode';
let raiseEventMethod: MethodHandle;
-let getComponentRenderInfoMethod: MethodHandle;
+let renderComponentMethod: MethodHandle;
// 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 } = {};
+type ComponentIdToParentElement = { [componentId: number]: Element };
+type BrowserRendererRegistry = { [browserRendererId: number]: ComponentIdToParentElement };
+const browserRenderers: BrowserRendererRegistry = {};
registerFunction('_blazorAttachComponentToElement', attachComponentToElement);
registerFunction('_blazorRender', renderRenderTree);
-function attachComponentToElement(elementSelector: System_String, componentId: System_String) {
+function attachComponentToElement(browserRendererId: number, elementSelector: System_String, componentId: number) {
const elementSelectorJs = platform.toJavaScriptString(elementSelector);
const element = document.querySelector(elementSelectorJs);
if (!element) {
throw new Error(`Could not find any element matching selector '${elementSelectorJs}'.`);
}
- const componentIdJs = platform.toJavaScriptString(componentId);
- componentIdToParentElement[componentIdJs] = element;
+ browserRenderers[browserRendererId] = browserRenderers[browserRendererId] || {};
+ browserRenderers[browserRendererId][componentId] = element;
}
-function renderRenderTree(componentId: System_String, tree: System_Array, treeLength: number) {
- const componentIdJs = platform.toJavaScriptString(componentId);
- const element = componentIdToParentElement[componentIdJs];
+function renderRenderTree(renderComponentArgs: Pointer) {
+ const browserRendererId = platform.readHeapInt32(renderComponentArgs, 0);
+ const browserRenderer = browserRenderers[browserRendererId];
+ if (!browserRenderer) {
+ throw new Error(`There is no browser renderer with ID ${browserRendererId}.`);
+ }
+
+ const componentId = platform.readHeapInt32(renderComponentArgs, 4);
+ const element = browserRenderer[componentId];
if (!element) {
- throw new Error(`No element is currently associated with component ${componentIdJs}`);
+ throw new Error(`No element is currently associated with component ${componentId}`);
}
clearElement(element);
- insertNodeRange(componentIdJs, element, tree, 0, treeLength - 1);
+
+ const tree = platform.readHeapObject(renderComponentArgs, 8) as System_Array;
+ const treeLength = platform.readHeapInt32(renderComponentArgs, 12);
+ insertNodeRange(browserRendererId, componentId, element, tree, 0, treeLength - 1);
}
-function insertNodeRange(componentId: string, intoDomElement: Element, tree: System_Array, startIndex: number, endIndex: number) {
+function insertNodeRange(browserRendererId: number, componentId: number, intoDomElement: Element, tree: System_Array, startIndex: number, endIndex: number) {
for (let index = startIndex; index <= endIndex; index++) {
const node = getTreeNodePtr(tree, index);
- insertNode(componentId, intoDomElement, tree, node, index);
+ insertNode(browserRendererId, componentId, intoDomElement, tree, node, index);
// Skip over any descendants, since they are already dealt with recursively
const descendantsEndIndex = renderTreeNode.descendantsEndIndex(node);
@@ -50,11 +61,11 @@ function insertNodeRange(componentId: string, intoDomElement: Element, tree: Sys
}
}
-function insertNode(componentId: string, intoDomElement: Element, tree: System_Array, node: RenderTreeNodePointer, nodeIndex: number) {
+function insertNode(browserRendererId: number, componentId: number, intoDomElement: Element, tree: System_Array, node: RenderTreeNodePointer, nodeIndex: number) {
const nodeType = renderTreeNode.nodeType(node);
switch (nodeType) {
case NodeType.element:
- insertElement(componentId, intoDomElement, tree, node, nodeIndex);
+ insertElement(browserRendererId, componentId, intoDomElement, tree, node, nodeIndex);
break;
case NodeType.text:
insertText(intoDomElement, node);
@@ -62,7 +73,7 @@ function insertNode(componentId: string, intoDomElement: Element, tree: System_A
case NodeType.attribute:
throw new Error('Attribute nodes should only be present as leading children of element nodes.');
case NodeType.component:
- insertComponent(intoDomElement, componentId, nodeIndex);
+ insertComponent(browserRendererId, intoDomElement, node);
break;
default:
const unknownType: never = nodeType; // Compile-time verification that the switch was exhaustive
@@ -70,31 +81,26 @@ function insertNode(componentId: string, intoDomElement: Element, tree: System_A
}
}
-function insertComponent(intoDomElement: Element, parentComponentId: string, componentNodeIndex: number) {
- if (!getComponentRenderInfoMethod) {
- getComponentRenderInfoMethod = platform.findMethod(
- 'Microsoft.Blazor.Browser', 'Microsoft.Blazor.Browser', 'DOMComponentRenderState', 'GetComponentRenderInfo'
+function insertComponent(browserRendererId: number, intoDomElement: Element, node: RenderTreeNodePointer) {
+ const containerElement = document.createElement('blazor-component');
+ intoDomElement.appendChild(containerElement);
+
+ var childComponentId = renderTreeNode.componentId(node);
+ browserRenderers[browserRendererId][childComponentId] = containerElement;
+
+ if (!renderComponentMethod) {
+ renderComponentMethod = platform.findMethod(
+ 'Microsoft.Blazor.Browser', 'Microsoft.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'RenderChildComponent'
);
}
- // Currently, platform.callMethod always returns a heap object. If the target method
- // tries to return a value, it gets boxed before return.
- const renderInfoBoxed = platform.callMethod(getComponentRenderInfoMethod, null, [
- platform.toDotNetString(parentComponentId),
- platform.toDotNetString(componentNodeIndex.toString())
+ platform.callMethod(renderComponentMethod, null, [
+ platform.toDotNetString(browserRendererId.toString()),
+ platform.toDotNetString(childComponentId.toString())
]);
- const renderInfoFields = platform.getHeapObjectFieldsPtr(renderInfoBoxed);
- const componentId = platform.toJavaScriptString(platform.readHeapObject(renderInfoFields, 0) as System_String);
- const componentTree = platform.readHeapObject(renderInfoFields, 4) as System_Array;
- const componentTreeLength = platform.readHeapInt32(renderInfoFields, 8);
-
- const containerElement = document.createElement('blazor-component');
- intoDomElement.appendChild(containerElement);
- componentIdToParentElement[componentId] = containerElement;
- insertNodeRange(componentId, containerElement, componentTree, 0, componentTreeLength - 1);
}
-function insertElement(componentId: string, intoDomElement: Element, tree: System_Array, elementNode: RenderTreeNodePointer, elementNodeIndex: number) {
+function insertElement(browserRendererId: number, componentId: number, intoDomElement: Element, tree: System_Array, elementNode: RenderTreeNodePointer, elementNodeIndex: number) {
const tagName = renderTreeNode.elementName(elementNode);
const newDomElement = document.createElement(tagName);
intoDomElement.appendChild(newDomElement);
@@ -104,22 +110,22 @@ function insertElement(componentId: string, intoDomElement: Element, tree: Syste
for (let descendantIndex = elementNodeIndex + 1; descendantIndex <= descendantsEndIndex; descendantIndex++) {
const descendantNode = getTreeNodePtr(tree, descendantIndex);
if (renderTreeNode.nodeType(descendantNode) === NodeType.attribute) {
- applyAttribute(componentId, newDomElement, descendantNode, descendantIndex);
+ applyAttribute(browserRendererId, componentId, newDomElement, descendantNode, descendantIndex);
} else {
// As soon as we see a non-attribute child, all the subsequent child nodes are
// not attributes, so bail out and insert the remnants recursively
- insertNodeRange(componentId, newDomElement, tree, descendantIndex, descendantsEndIndex);
+ insertNodeRange(browserRendererId, componentId, newDomElement, tree, descendantIndex, descendantsEndIndex);
break;
}
}
}
-function applyAttribute(componentId: string, toDomElement: Element, attributeNode: RenderTreeNodePointer, attributeNodeIndex: number) {
+function applyAttribute(browserRendererId: number, componentId: number, toDomElement: Element, attributeNode: RenderTreeNodePointer, attributeNodeIndex: number) {
const attributeName = renderTreeNode.attributeName(attributeNode);
switch (attributeName) {
case 'onclick':
- toDomElement.addEventListener('click', () => raiseEvent(componentId, attributeNodeIndex, 'mouse', { Type: 'click' }));
+ toDomElement.addEventListener('click', () => raiseEvent(browserRendererId, componentId, attributeNodeIndex, 'mouse', { Type: 'click' }));
break;
case 'onkeypress':
toDomElement.addEventListener('keypress', evt => {
@@ -127,7 +133,7 @@ function applyAttribute(componentId: string, toDomElement: Element, attributeNod
// just to establish that we can pass parameters when raising events.
// We use C#-style PascalCase on the eventInfo to simplify deserialization, but this could
// change if we introduced a richer JSON library on the .NET side.
- raiseEvent(componentId, attributeNodeIndex, 'keyboard', { Type: evt.type, Key: (evt as any).key });
+ raiseEvent(browserRendererId, componentId, attributeNodeIndex, 'keyboard', { Type: evt.type, Key: (evt as any).key });
});
break;
default:
@@ -140,19 +146,22 @@ function applyAttribute(componentId: string, toDomElement: Element, attributeNod
}
}
-function raiseEvent(componentId: string, renderTreeNodeIndex: number, eventInfoType: EventInfoType, eventInfo: any) {
+function raiseEvent(browserRendererId: number, componentId: number, renderTreeNodeIndex: number, eventInfoType: EventInfoType, eventInfo: any) {
if (!raiseEventMethod) {
raiseEventMethod = platform.findMethod(
- 'Microsoft.Blazor.Browser', 'Microsoft.Blazor.Browser', 'Events', 'RaiseEvent'
+ 'Microsoft.Blazor.Browser', 'Microsoft.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'DispatchEvent'
);
}
- // TODO: Find a way of passing the renderTreeNodeIndex as a System.Int32, possibly boxing
- // it first if necessary. Until then we have to send it as a string.
+ const eventDescriptor = {
+ BrowserRendererId: browserRendererId,
+ ComponentId: componentId,
+ RenderTreeNodeIndex: renderTreeNodeIndex,
+ EventArgsType: eventInfoType
+ };
+
platform.callMethod(raiseEventMethod, null, [
- platform.toDotNetString(componentId),
- platform.toDotNetString(renderTreeNodeIndex.toString()),
- platform.toDotNetString(eventInfoType),
+ platform.toDotNetString(JSON.stringify(eventDescriptor)),
platform.toDotNetString(JSON.stringify(eventInfo))
]);
}
diff --git a/src/Microsoft.Blazor.Browser/DOM.cs b/src/Microsoft.Blazor.Browser/DOM.cs
deleted file mode 100644
index 5e27c40640..0000000000
--- a/src/Microsoft.Blazor.Browser/DOM.cs
+++ /dev/null
@@ -1,31 +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;
-
-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);
-
- renderState.RenderToDOM();
- }
- }
-}
diff --git a/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs b/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs
deleted file mode 100644
index f1bffb15b8..0000000000
--- a/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs
+++ /dev/null
@@ -1,131 +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.RenderTree;
-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
- {
- // Track the associations between component IDs, IComponent instances, and
- // DOMComponentRenderState instances, but without pinning any IComponent instances
- // in memory.
- // TODO: Instead of storing these as statics, have some kind of RenderContext instance
- // that holds them. It can also hold a reference to the root component, since otherwise
- // there isn't anything stopping the whole hierarchy of components from being GCed.
- private static ConditionalWeakTable _renderStatesByComponent
- = new ConditionalWeakTable();
- private static WeakValueDictionary _renderStatesByComponentId
- = new WeakValueDictionary();
- private static long _nextDOMComponentId = 0;
-
- private readonly RenderTreeBuilder _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 = componentId;
- Component = component;
- _uITreeBuilder = new RenderTreeBuilder();
- }
-
- public static DOMComponentRenderState GetOrCreate(IComponent component)
- {
- lock (_renderStatesByComponent)
- {
- if (_renderStatesByComponent.TryGetValue(component, out var existingState))
- {
- return existingState;
- }
- else
- {
- var newId = (_nextDOMComponentId++).ToString();
- var newState = new DOMComponentRenderState(newId, component);
- _renderStatesByComponent.Add(component, newState);
- _renderStatesByComponentId.Add(newId, newState);
- return newState;
- }
- }
- }
-
- public static DOMComponentRenderState FindByDOMComponentID(string id)
- => _renderStatesByComponentId.TryGetValue(id, out var result)
- ? result
- : throw new ArgumentException($"No component was found with ID {id}");
-
- private ArraySegment UpdateRender()
- {
- _uITreeBuilder.Clear();
- Component.BuildRenderTree(_uITreeBuilder);
-
- // TODO: Change this to return a diff between the previous render result and this new one
- return _uITreeBuilder.GetNodes();
- }
-
- public void RaiseEvent(int uiTreeNodeIndex, UIEventArgs eventInfo)
- {
- var nodes = _uITreeBuilder.GetNodes();
- var eventHandler = nodes.Array[nodes.Offset + uiTreeNodeIndex].AttributeEventHandlerValue;
- if (eventHandler == null)
- {
- throw new ArgumentException($"Cannot raise event because the specified {nameof(RenderTreeNode)} at index {uiTreeNodeIndex} does not have any {nameof(RenderTreeNode.AttributeEventHandlerValue)}.");
- }
-
- eventHandler.Invoke(eventInfo);
- RenderToDOM();
- }
-
- public void RenderToDOM()
- {
- var tree = UpdateRender();
- RegisteredFunction.InvokeUnmarshalled(
- "_blazorRender",
- DOMComponentId,
- tree.Array,
- tree.Count);
- }
-
- public static ComponentRenderInfo GetComponentRenderInfo(string parentComponentId, string componentNodeIndexString)
- {
- var parentComponentRenderState = FindByDOMComponentID(parentComponentId);
- var parentComponentNodes = parentComponentRenderState._uITreeBuilder.GetNodes();
- var component = parentComponentNodes
- .Array[int.Parse(componentNodeIndexString)]
- .Component;
- if (component == null )
- {
- throw new ArgumentException($"The tree entry at position {componentNodeIndexString} does not refer to a component.");
- }
-
- var componentRenderState = GetOrCreate(component);
-
- // Don't necessarily need to re-render the child at this point. Review when the
- // component lifecycle has more details (e.g., async init method)
- var componentNodes = componentRenderState.UpdateRender();
- return new ComponentRenderInfo
- {
- ComponentId = componentRenderState.DOMComponentId,
- RenderTree = componentNodes.Array,
- RenderTreeLength = componentNodes.Count
- };
- }
-
- public struct ComponentRenderInfo
- {
- public string ComponentId;
- public RenderTreeNode[] RenderTree;
- public int RenderTreeLength;
- }
- }
-}
diff --git a/src/Microsoft.Blazor.Browser/Events.cs b/src/Microsoft.Blazor.Browser/Events.cs
deleted file mode 100644
index b60bf254a9..0000000000
--- a/src/Microsoft.Blazor.Browser/Events.cs
+++ /dev/null
@@ -1,36 +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.RenderTree;
-using System;
-
-namespace Microsoft.Blazor.Browser
-{
- // Invoked by the Microsoft.Blazor.Browser.JS code when a DOM event occurs
- internal static class Events
- {
- public static void RaiseEvent(string domComponentID, string uiTreeNodeIndex, string eventInfoType, string eventInfoJson)
- {
- // We're receiving the uiTreeNodeIndex as a string only because there's not
- // yet a way to pass ints (or construct boxed ones) from JS with the current Mono
- // runtime. When there's a supported way to do that, this can be simplified.
- var renderState = DOMComponentRenderState.FindByDOMComponentID(domComponentID);
- var eventInfo = ParseEventInfo(eventInfoType, eventInfoJson);
- renderState.RaiseEvent(int.Parse(uiTreeNodeIndex), eventInfo);
- }
-
- private static UIEventArgs ParseEventInfo(string eventInfoType, string eventInfoJson)
- {
- switch (eventInfoType)
- {
- case "mouse":
- return Json.Deserialize(eventInfoJson);
- case "keyboard":
- return Json.Deserialize(eventInfoJson);
- default:
- throw new ArgumentException($"Unsupported value '{eventInfoType}'.", nameof(eventInfoType));
- }
- }
- }
-}
diff --git a/src/Microsoft.Blazor.Browser/Rendering/BrowserRenderer.cs b/src/Microsoft.Blazor.Browser/Rendering/BrowserRenderer.cs
new file mode 100644
index 0000000000..367dc46f07
--- /dev/null
+++ b/src/Microsoft.Blazor.Browser/Rendering/BrowserRenderer.cs
@@ -0,0 +1,87 @@
+// 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.Rendering;
+using Microsoft.Blazor.RenderTree;
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Blazor.Browser.Rendering
+{
+ public class BrowserRenderer : Renderer, IDisposable
+ {
+ private readonly int _browserRendererId;
+
+ // Ensures the explicitly-added components aren't GCed, because the browser
+ // will still send events referencing them by ID. We only need to store the
+ // top-level components, because the associated ComponentState will reference
+ // all the reachable descendant components of each.
+ private IList _rootComponents = new List();
+
+ ///
+ /// Constructs an instance of .
+ ///
+ public BrowserRenderer()
+ {
+ _browserRendererId = BrowserRendererRegistry.Add(this);
+ }
+
+ internal void DispatchBrowserEvent(int componentId, int renderTreeIndex, UIEventArgs eventArgs)
+ => DispatchEvent(componentId, renderTreeIndex, eventArgs);
+
+ internal void RenderComponentInternal(int componentId)
+ => RenderComponent(componentId);
+
+ ///
+ /// Associates the with the ,
+ /// causing it to be displayed in the specified DOM element.
+ ///
+ /// A CSS selector that uniquely identifies a DOM element.
+ /// The .
+ public void AddComponent(string domElementSelector, IComponent component)
+ {
+ var componentId = AssignComponentId(component);
+ RegisteredFunction.InvokeUnmarshalled(
+ "_blazorAttachComponentToElement",
+ _browserRendererId,
+ domElementSelector,
+ componentId);
+ _rootComponents.Add(component);
+
+ RenderComponent(component);
+ }
+
+ ///
+ public void Dispose()
+ {
+ BrowserRendererRegistry.TryRemove(_browserRendererId);
+ }
+
+ ///
+ protected override void UpdateDisplay(
+ int componentId,
+ ArraySegment renderTree)
+ {
+ RegisteredFunction.InvokeUnmarshalled(
+ "_blazorRender",
+ new RenderComponentArgs
+ {
+ BrowserRendererId = _browserRendererId,
+ ComponentId = componentId,
+ RenderTree = renderTree.Array,
+ RenderTreeLength = renderTree.Count
+ });
+ }
+
+ // Encapsulates the data we pass to the JS rendering function
+ private struct RenderComponentArgs
+ {
+ public int BrowserRendererId;
+ public int ComponentId;
+ public RenderTreeNode[] RenderTree;
+ public int RenderTreeLength;
+ }
+ }
+}
diff --git a/src/Microsoft.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs b/src/Microsoft.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs
new file mode 100644
index 0000000000..20a92cc732
--- /dev/null
+++ b/src/Microsoft.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs
@@ -0,0 +1,63 @@
+// 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.RenderTree;
+using System;
+
+namespace Microsoft.Blazor.Browser.Rendering
+{
+ ///
+ /// Provides mechanisms for dispatching events to components in a .
+ /// This is marked 'internal' because it only gets invoked from JS code.
+ ///
+ internal static class BrowserRendererEventDispatcher
+ {
+ // We receive the information as JSON strings because of current interop limitations:
+ // - Can't pass unboxed value types from JS to .NET (yet all the IDs are ints)
+ // - Can't pass more than 4 args from JS to .NET
+ // This can be simplified in the future when the Mono WASM runtime is enhanced.
+ public static void DispatchEvent(string eventDescriptorJson, string eventArgsJson)
+ {
+ var eventDescriptor = Json.Deserialize(eventDescriptorJson);
+ var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
+ var browserRenderer = BrowserRendererRegistry.Find(eventDescriptor.BrowserRendererId);
+ browserRenderer.DispatchBrowserEvent(
+ eventDescriptor.ComponentId,
+ eventDescriptor.RenderTreeNodeIndex,
+ eventArgs);
+ }
+
+ // Again, the params are received as strings for the same reason as above and
+ // can be simplified once runtime support improves.
+ public static void RenderChildComponent(
+ string browserRendererId,
+ string componentId)
+ {
+ var browserRenderer = BrowserRendererRegistry.Find(int.Parse(browserRendererId));
+ browserRenderer.RenderComponentInternal(
+ int.Parse(componentId));
+ }
+
+ private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson)
+ {
+ switch (eventArgsType)
+ {
+ case "mouse":
+ return Json.Deserialize(eventArgsJson);
+ case "keyboard":
+ return Json.Deserialize(eventArgsJson);
+ default:
+ throw new ArgumentException($"Unsupported value '{eventArgsType}'.", nameof(eventArgsType));
+ }
+ }
+
+ private class BrowserEventDescriptor
+ {
+ public int BrowserRendererId { get; set; }
+ public int ComponentId { get; set; }
+ public int RenderTreeNodeIndex { get; set; }
+ public string EventArgsType { get; set; }
+ }
+ }
+}
diff --git a/src/Microsoft.Blazor.Browser/Rendering/BrowserRendererRegistry.cs b/src/Microsoft.Blazor.Browser/Rendering/BrowserRendererRegistry.cs
new file mode 100644
index 0000000000..0e784cd043
--- /dev/null
+++ b/src/Microsoft.Blazor.Browser/Rendering/BrowserRendererRegistry.cs
@@ -0,0 +1,71 @@
+// 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 System.Collections.Generic;
+
+namespace Microsoft.Blazor.Browser.Rendering
+{
+ ///
+ /// Provides mechanisms for locating instances
+ /// by ID. This is used when receiving incoming events from the browser. It
+ /// implictly ensures that the instances and
+ /// their associated component instances aren't GCed when events may still
+ /// be received for them.
+ ///
+ internal static class BrowserRendererRegistry
+ {
+ private static int _nextId;
+ private static IDictionary _browserRenderers
+ = new Dictionary();
+
+ ///
+ /// Adds the and gets a unique identifier for it.
+ ///
+ ///
+ /// A unique identifier for the .
+ public static int Add(BrowserRenderer browserRenderer)
+ {
+ lock (_browserRenderers)
+ {
+ var id = _nextId++;
+ _browserRenderers.Add(id, browserRenderer);
+ return id;
+ }
+ }
+
+ ///
+ /// Gets the with the specified
+ /// .
+ ///
+ /// The identifier of the instance to be returned.
+ /// The corresponding instance.
+ public static BrowserRenderer Find(int browserRendererId)
+ {
+ lock (_browserRenderers)
+ {
+ return _browserRenderers[browserRendererId];
+ }
+ }
+
+ ///
+ /// Removes the with the specified identifier, if present.
+ ///
+ /// The identifier of the to remove.
+ /// if the was present; otherwise .
+ public static bool TryRemove(int browserRendererId)
+ {
+ lock (_browserRenderers)
+ {
+ if (_browserRenderers.ContainsKey(browserRendererId))
+ {
+ _browserRenderers.Remove(browserRendererId);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs
index a6dce7911f..70df197b7b 100644
--- a/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs
+++ b/src/Microsoft.Blazor/RenderTree/RenderTreeBuilder.cs
@@ -2,6 +2,7 @@
// 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.Rendering;
using System;
using System.Collections.Generic;
@@ -13,11 +14,21 @@ namespace Microsoft.Blazor.RenderTree
public class RenderTreeBuilder
{
private const int MinBufferLength = 10;
+ private readonly Renderer _renderer;
private RenderTreeNode[] _entries = new RenderTreeNode[100];
private int _entriesInUse = 0;
- private Stack _openElementIndices = new Stack();
+ private readonly Stack _openElementIndices = new Stack();
private RenderTreeNodeType? _lastNonAttributeNodeType;
+ ///
+ /// Constructs an instance of .
+ ///
+ /// The associated .
+ public RenderTreeBuilder(Renderer renderer)
+ {
+ _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
+ }
+
///
/// Appends a node representing an element, i.e., a container for other nodes.
/// In order for the state to be valid, you must
@@ -84,7 +95,8 @@ namespace Microsoft.Blazor.RenderTree
// previous tree, we'll either instantiate a new component or reuse the
// existing instance (and notify it about changes to parameters).
var instance = Activator.CreateInstance();
- Append(RenderTreeNode.ChildComponent(instance));
+ var instanceId = _renderer.AssignComponentId(instance);
+ Append(RenderTreeNode.ChildComponent(instanceId, instance));
}
private void AssertCanAddAttribute()
diff --git a/src/Microsoft.Blazor/RenderTree/RenderTreeNode.cs b/src/Microsoft.Blazor/RenderTree/RenderTreeNode.cs
index c628903120..c27819c3a8 100644
--- a/src/Microsoft.Blazor/RenderTree/RenderTreeNode.cs
+++ b/src/Microsoft.Blazor/RenderTree/RenderTreeNode.cs
@@ -56,6 +56,12 @@ namespace Microsoft.Blazor.RenderTree
///
public UIEventHandler AttributeEventHandlerValue { get; private set; }
+ ///
+ /// If the property equals ,
+ /// gets the child component instance identifier.
+ ///
+ public int ComponentId { get; private set; }
+
///
/// If the property equals ,
/// gets the child component instance. Otherwise, the value is .
@@ -88,9 +94,10 @@ namespace Microsoft.Blazor.RenderTree
AttributeEventHandlerValue = value
};
- internal static RenderTreeNode ChildComponent(IComponent component) => new RenderTreeNode
+ internal static RenderTreeNode ChildComponent(int componentId, IComponent component) => new RenderTreeNode
{
NodeType = RenderTreeNodeType.Component,
+ ComponentId = componentId,
Component = component
};
diff --git a/src/Microsoft.Blazor/Rendering/ComponentState.cs b/src/Microsoft.Blazor/Rendering/ComponentState.cs
new file mode 100644
index 0000000000..e5826d8180
--- /dev/null
+++ b/src/Microsoft.Blazor/Rendering/ComponentState.cs
@@ -0,0 +1,75 @@
+// 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.RenderTree;
+using System;
+
+namespace Microsoft.Blazor.Rendering
+{
+ ///
+ /// Tracks the rendering state associated with an instance
+ /// within the context of a . This is an internal implementation
+ /// detail of .
+ ///
+ internal class ComponentState
+ {
+ private readonly int _componentId; // TODO: Change the type to 'long' when the Mono runtime has more complete support for passing longs in .NET->JS calls
+ private readonly IComponent _component;
+ private readonly Renderer _renderer;
+ private readonly RenderTreeBuilder _renderTreeBuilder;
+
+ ///
+ /// Constructs an instance of .
+ ///
+ /// The with which the new instance should be associated.
+ /// The externally visible identifer for the . The identifier must be unique in the context of the .
+ /// The whose state is being tracked.
+ public ComponentState(Renderer renderer, int componentId, IComponent component)
+ {
+ _componentId = componentId;
+ _component = component ?? throw new ArgumentNullException(nameof(component));
+ _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
+ _renderTreeBuilder = new RenderTreeBuilder(renderer);
+ }
+
+ ///
+ /// Regenerates the and notifies the
+ /// to update the visible UI state.
+ ///
+ public void Render()
+ {
+ _renderTreeBuilder.Clear();
+ _component.BuildRenderTree(_renderTreeBuilder);
+
+ var renderTree = _renderTreeBuilder.GetNodes();
+ _renderer.UpdateDisplay(_componentId, renderTree);
+ }
+
+ ///
+ /// Invokes the handler corresponding to an event.
+ ///
+ /// The index of the current render tree node that holds the event handler to be invoked.
+ /// Arguments to be passed to the event handler.
+ public void DispatchEvent(int renderTreeIndex, UIEventArgs eventArgs)
+ {
+ if (eventArgs == null)
+ {
+ throw new ArgumentNullException(nameof(eventArgs));
+ }
+
+ var nodes = _renderTreeBuilder.GetNodes();
+ var eventHandler = nodes.Array[renderTreeIndex].AttributeEventHandlerValue;
+ if (eventHandler == null)
+ {
+ throw new ArgumentException($"The render tree node at index {renderTreeIndex} has a null value for {nameof(RenderTreeNode.AttributeEventHandlerValue)}.");
+ }
+
+ eventHandler.Invoke(eventArgs);
+
+ // After any event, we synchronously re-render. Most of the time this means that
+ // developers don't need to call Render() on their components explicitly.
+ Render();
+ }
+ }
+}
diff --git a/src/Microsoft.Blazor/Rendering/Renderer.cs b/src/Microsoft.Blazor/Rendering/Renderer.cs
new file mode 100644
index 0000000000..d9f6ebf621
--- /dev/null
+++ b/src/Microsoft.Blazor/Rendering/Renderer.cs
@@ -0,0 +1,97 @@
+// 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.RenderTree;
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.Blazor.Rendering
+{
+ ///
+ /// Provides mechanisms for rendering hierarchies of instances,
+ /// dispatching events to them, and notifying when the user interface is being updated.
+ ///
+ public abstract class Renderer
+ {
+ // Methods for tracking associations between component IDs, instances, and states,
+ // without pinning any of them in memory here. The explictly GC rooted items are the
+ // components explicitly added to the renderer (i.e., top-level components). In turn
+ // these reference descendant components and associated ComponentState instances.
+ private readonly WeakValueDictionary _componentStateById
+ = new WeakValueDictionary();
+ private readonly ConditionalWeakTable _componentStateByComponent
+ = new ConditionalWeakTable();
+ private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
+
+ // Ensure that explictly-added components (and transitively, their current descendants)
+ // aren't GCed. If we don't do this then the display layer (e.g., browser) might send
+ // us events for component IDs where the corresponding state has already been collected.
+ private readonly IList _topLevelComponents = new List();
+
+ ///
+ /// Associates the with the , assigning
+ /// an identifier that is unique within the scope of the .
+ ///
+ /// The .
+ /// The assigned identifier for the .
+ protected internal int AssignComponentId(IComponent component)
+ {
+ lock (_componentStateByComponent)
+ {
+ if (_componentStateByComponent.TryGetValue(component, out _))
+ {
+ throw new ArgumentException("The component was already associated with the renderer.");
+ }
+
+ var componentId = _nextComponentId++;
+ var componentState = new ComponentState(this, componentId, component);
+ _componentStateById.Add(componentId, componentState);
+ _componentStateByComponent.Add(component, componentState);
+ return componentId;
+ }
+ }
+
+ ///
+ /// Updates the visible UI to display the supplied
+ /// at the location corresponding to the .
+ ///
+ /// The identifier for the updated .
+ /// The updated render tree to be displayed.
+ internal protected abstract void UpdateDisplay(int componentId, ArraySegment renderTree);
+
+ ///
+ /// Updates the rendered state of the specified .
+ ///
+ /// The .
+ protected void RenderComponent(IComponent component)
+ => GetRequiredComponentState(component).Render();
+
+ ///
+ /// Updates the rendered state of the specified .
+ ///
+ /// The identifier of the to render.
+ protected void RenderComponent(int componentId)
+ => GetRequiredComponentState(componentId).Render();
+
+ ///
+ /// Notifies the specified component that an event has occurred.
+ ///
+ /// The unique identifier for the component within the scope of this .
+ /// The index into the component's current render tree that specifies which event handler to invoke.
+ /// Arguments to be passed to the event handler.
+ protected void DispatchEvent(int componentId, int renderTreeIndex, UIEventArgs eventArgs)
+ => GetRequiredComponentState(componentId).DispatchEvent(renderTreeIndex, eventArgs);
+
+ private ComponentState GetRequiredComponentState(int componentId)
+ => _componentStateById.TryGetValue(componentId, out var componentState)
+ ? componentState
+ : throw new ArgumentException($"The renderer does not have a component with ID {componentId}.");
+
+ private ComponentState GetRequiredComponentState(IComponent component)
+ => _componentStateByComponent.TryGetValue(component, out var componentState)
+ ? componentState
+ : throw new ArgumentException("The component is not associated with the renderer.");
+ }
+}
diff --git a/src/Microsoft.Blazor.Browser/WeakValueDictionary.cs b/src/Microsoft.Blazor/Rendering/WeakValueDictionary.cs
similarity index 98%
rename from src/Microsoft.Blazor.Browser/WeakValueDictionary.cs
rename to src/Microsoft.Blazor/Rendering/WeakValueDictionary.cs
index c98d3f7087..43303b2f2f 100644
--- a/src/Microsoft.Blazor.Browser/WeakValueDictionary.cs
+++ b/src/Microsoft.Blazor/Rendering/WeakValueDictionary.cs
@@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
-namespace Microsoft.Blazor.Browser
+namespace Microsoft.Blazor.Rendering
{
internal class WeakValueDictionary where TValue : class
{
diff --git a/test/Microsoft.Blazor.Test/UITreeBuilderTest.cs b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs
similarity index 90%
rename from test/Microsoft.Blazor.Test/UITreeBuilderTest.cs
rename to test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs
index b8e89e9885..1243462771 100644
--- a/test/Microsoft.Blazor.Test/UITreeBuilderTest.cs
+++ b/test/Microsoft.Blazor.Test/RenderTreeBuilderTest.cs
@@ -2,6 +2,7 @@
// 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.Rendering;
using Microsoft.Blazor.RenderTree;
using System;
using System.Linq;
@@ -15,7 +16,7 @@ namespace Microsoft.Blazor.Test
public void StartsEmpty()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Assert
var nodes = builder.GetNodes();
@@ -28,7 +29,7 @@ namespace Microsoft.Blazor.Test
public void CanAddText()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.AddText("First item");
@@ -46,7 +47,7 @@ namespace Microsoft.Blazor.Test
public void UnclosedElementsHaveNoEndDescendantIndex()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.OpenElement("my element");
@@ -60,7 +61,7 @@ namespace Microsoft.Blazor.Test
public void ClosedEmptyElementsHaveSelfAsEndDescendantIndex()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.AddText("some node so that the element isn't at position zero");
@@ -77,7 +78,7 @@ namespace Microsoft.Blazor.Test
public void ClosedElementsHaveCorrectEndDescendantIndex()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.OpenElement("my element");
@@ -96,7 +97,7 @@ namespace Microsoft.Blazor.Test
public void CanNestElements()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.AddText("standalone text 1"); // 0: standalone text 1
@@ -136,7 +137,7 @@ namespace Microsoft.Blazor.Test
public void CanAddAttributes()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
UIEventHandler eventHandler = eventInfo => { };
// Act
@@ -163,7 +164,7 @@ namespace Microsoft.Blazor.Test
public void CannotAddAttributeAtRoot()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws(() =>
@@ -176,7 +177,7 @@ namespace Microsoft.Blazor.Test
public void CannotAddEventHandlerAttributeAtRoot()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws(() =>
@@ -189,7 +190,7 @@ namespace Microsoft.Blazor.Test
public void CannotAddAttributeToText()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws(() =>
@@ -204,7 +205,7 @@ namespace Microsoft.Blazor.Test
public void CannotAddEventHandlerAttributeToText()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws(() =>
@@ -219,7 +220,7 @@ namespace Microsoft.Blazor.Test
public void CanAddChildComponents()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.OpenElement("parent"); // 0:
@@ -244,7 +245,7 @@ namespace Microsoft.Blazor.Test
public void CanClear()
{
// Arrange
- var builder = new RenderTreeBuilder();
+ var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.AddText("some text");
@@ -306,5 +307,11 @@ namespace Microsoft.Blazor.Test
public void BuildRenderTree(RenderTreeBuilder builder)
=> throw new NotImplementedException();
}
+
+ private class TestRenderer : Renderer
+ {
+ protected override void UpdateDisplay(int componentId, ArraySegment renderTree)
+ => throw new NotImplementedException();
+ }
}
}
diff --git a/test/testapps/BasicTestApp/Program.cs b/test/testapps/BasicTestApp/Program.cs
index 623ef67260..ee3ebc1a50 100644
--- a/test/testapps/BasicTestApp/Program.cs
+++ b/test/testapps/BasicTestApp/Program.cs
@@ -1,8 +1,8 @@
// 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;
using Microsoft.Blazor.Browser.Interop;
+using Microsoft.Blazor.Browser.Rendering;
using Microsoft.Blazor.Components;
using System;
@@ -20,7 +20,7 @@ namespace BasicTestApp
{
var componentType = Type.GetType(componentTypeName);
var componentInstance = (IComponent)Activator.CreateInstance(componentType);
- DOM.AttachComponent("app", componentInstance);
+ new BrowserRenderer().AddComponent("app", componentInstance);
}
}
}