Refactor most of the rendering logic into Microsoft.Blazor, keeping only browser-specific parts in Microsoft.Blazor.Browser

This commit is contained in:
Steve Sanderson 2018-01-09 13:33:49 +00:00
parent 674024ed61
commit 6585667ce6
17 changed files with 509 additions and 266 deletions

View File

@ -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());
}
}

View File

@ -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 {

View File

@ -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);

View File

@ -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))
]);
}

View File

@ -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
{
/// <summary>
/// Provides mechanisms for displaying Blazor components in a browser Document
/// Object Model (DOM).
/// </summary>
public static class DOM
{
/// <summary>
/// Associates the specified component with the specified DOM element, causing the
/// component to be displayed there.
/// </summary>
/// <param name="elementSelector">A CSS selector that identifies a unique DOM element.</param>
/// <param name="component">The component to be displayed in the DOM element.</param>
public static void AttachComponent(string elementSelector, IComponent component)
{
var renderState = DOMComponentRenderState.GetOrCreate(component);
RegisteredFunction.InvokeUnmarshalled<string, string, object>(
"_blazorAttachComponentToElement", elementSelector, renderState.DOMComponentId);
renderState.RenderToDOM();
}
}
}

View File

@ -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
{
/// <summary>
/// Tracks the rendering state associated with an <see cref="IComponent"/> that is
/// being displayed in the DOM.
/// </summary>
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<IComponent, DOMComponentRenderState> _renderStatesByComponent
= new ConditionalWeakTable<IComponent, DOMComponentRenderState>();
private static WeakValueDictionary<string, DOMComponentRenderState> _renderStatesByComponentId
= new WeakValueDictionary<string, DOMComponentRenderState>();
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<RenderTreeNode> 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<string, RenderTreeNode[], int, object>(
"_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;
}
}
}

View File

@ -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<UIMouseEventArgs>(eventInfoJson);
case "keyboard":
return Json.Deserialize<UIKeyboardEventArgs>(eventInfoJson);
default:
throw new ArgumentException($"Unsupported value '{eventInfoType}'.", nameof(eventInfoType));
}
}
}
}

View File

@ -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<IComponent> _rootComponents = new List<IComponent>();
/// <summary>
/// Constructs an instance of <see cref="BrowserRenderer"/>.
/// </summary>
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);
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="BrowserRenderer"/>,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
/// <param name="component">The <see cref="IComponent"/>.</param>
public void AddComponent(string domElementSelector, IComponent component)
{
var componentId = AssignComponentId(component);
RegisteredFunction.InvokeUnmarshalled<int, string, int, object>(
"_blazorAttachComponentToElement",
_browserRendererId,
domElementSelector,
componentId);
_rootComponents.Add(component);
RenderComponent(component);
}
/// <inheritdoc />
public void Dispose()
{
BrowserRendererRegistry.TryRemove(_browserRendererId);
}
/// <inheritdoc />
protected override void UpdateDisplay(
int componentId,
ArraySegment<RenderTreeNode> renderTree)
{
RegisteredFunction.InvokeUnmarshalled<RenderComponentArgs, object>(
"_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;
}
}
}

View File

@ -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
{
/// <summary>
/// Provides mechanisms for dispatching events to components in a <see cref="BrowserRenderer"/>.
/// This is marked 'internal' because it only gets invoked from JS code.
/// </summary>
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<BrowserEventDescriptor>(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<UIMouseEventArgs>(eventArgsJson);
case "keyboard":
return Json.Deserialize<UIKeyboardEventArgs>(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; }
}
}
}

View File

@ -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
{
/// <summary>
/// Provides mechanisms for locating <see cref="BrowserRenderer"/> instances
/// by ID. This is used when receiving incoming events from the browser. It
/// implictly ensures that the <see cref="BrowserRenderer"/> instances and
/// their associated component instances aren't GCed when events may still
/// be received for them.
/// </summary>
internal static class BrowserRendererRegistry
{
private static int _nextId;
private static IDictionary<int, BrowserRenderer> _browserRenderers
= new Dictionary<int, BrowserRenderer>();
/// <summary>
/// Adds the <paramref name="browserRenderer"/> and gets a unique identifier for it.
/// </summary>
/// <param name="browserRenderer"></param>
/// <returns>A unique identifier for the <paramref name="browserRenderer"/>.</returns>
public static int Add(BrowserRenderer browserRenderer)
{
lock (_browserRenderers)
{
var id = _nextId++;
_browserRenderers.Add(id, browserRenderer);
return id;
}
}
/// <summary>
/// Gets the <see cref="BrowserRenderer"/> with the specified
/// <paramref name="browserRendererId"/>.
/// </summary>
/// <param name="browserRendererId">The identifier of the instance to be returned.</param>
/// <returns>The corresponding <see cref="BrowserRenderer"/> instance.</returns>
public static BrowserRenderer Find(int browserRendererId)
{
lock (_browserRenderers)
{
return _browserRenderers[browserRendererId];
}
}
/// <summary>
/// Removes the <see cref="BrowserRenderer"/> with the specified identifier, if present.
/// </summary>
/// <param name="browserRendererId">The identifier of the <see cref="BrowserRenderer"/> to remove.</param>
/// <returns><see langword="true"/> if the <see cref="BrowserRenderer"/> was present; otherwise <see langword="false" />.</returns>
public static bool TryRemove(int browserRendererId)
{
lock (_browserRenderers)
{
if (_browserRenderers.ContainsKey(browserRendererId))
{
_browserRenderers.Remove(browserRendererId);
return true;
}
else
{
return false;
}
}
}
}
}

View File

@ -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<int> _openElementIndices = new Stack<int>();
private readonly Stack<int> _openElementIndices = new Stack<int>();
private RenderTreeNodeType? _lastNonAttributeNodeType;
/// <summary>
/// Constructs an instance of <see cref="RenderTreeBuilder"/>.
/// </summary>
/// <param name="renderer">The associated <see cref="Renderer"/>.</param>
public RenderTreeBuilder(Renderer renderer)
{
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
}
/// <summary>
/// Appends a node representing an element, i.e., a container for other nodes.
/// In order for the <see cref="RenderTreeBuilder"/> 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<TComponent>();
Append(RenderTreeNode.ChildComponent(instance));
var instanceId = _renderer.AssignComponentId(instance);
Append(RenderTreeNode.ChildComponent(instanceId, instance));
}
private void AssertCanAddAttribute()

View File

@ -56,6 +56,12 @@ namespace Microsoft.Blazor.RenderTree
/// </summary>
public UIEventHandler AttributeEventHandlerValue { get; private set; }
/// <summary>
/// If the <see cref="NodeType"/> property equals <see cref="RenderTreeNodeType.Component"/>,
/// gets the child component instance identifier.
/// </summary>
public int ComponentId { get; private set; }
/// <summary>
/// If the <see cref="NodeType"/> property equals <see cref="RenderTreeNodeType.Component"/>,
/// gets the child component instance. Otherwise, the value is <see langword="null"/>.
@ -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
};

View File

@ -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
{
/// <summary>
/// Tracks the rendering state associated with an <see cref="IComponent"/> instance
/// within the context of a <see cref="Renderer"/>. This is an internal implementation
/// detail of <see cref="Renderer"/>.
/// </summary>
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;
/// <summary>
/// Constructs an instance of <see cref="ComponentState"/>.
/// </summary>
/// <param name="renderer">The <see cref="Renderer"/> with which the new instance should be associated.</param>
/// <param name="componentId">The externally visible identifer for the <see cref="IComponent"/>. The identifier must be unique in the context of the <see cref="Renderer"/>.</param>
/// <param name="component">The <see cref="IComponent"/> whose state is being tracked.</param>
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);
}
/// <summary>
/// Regenerates the <see cref="RenderTree"/> and notifies the <see cref="Renderer"/>
/// to update the visible UI state.
/// </summary>
public void Render()
{
_renderTreeBuilder.Clear();
_component.BuildRenderTree(_renderTreeBuilder);
var renderTree = _renderTreeBuilder.GetNodes();
_renderer.UpdateDisplay(_componentId, renderTree);
}
/// <summary>
/// Invokes the handler corresponding to an event.
/// </summary>
/// <param name="renderTreeIndex">The index of the current render tree node that holds the event handler to be invoked.</param>
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
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();
}
}
}

View File

@ -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
{
/// <summary>
/// Provides mechanisms for rendering hierarchies of <see cref="IComponent"/> instances,
/// dispatching events to them, and notifying when the user interface is being updated.
/// </summary>
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<int, ComponentState> _componentStateById
= new WeakValueDictionary<int, ComponentState>();
private readonly ConditionalWeakTable<IComponent, ComponentState> _componentStateByComponent
= new ConditionalWeakTable<IComponent, ComponentState>();
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<IComponent> _topLevelComponents = new List<IComponent>();
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
/// an identifier that is unique within the scope of the <see cref="Renderer"/>.
/// </summary>
/// <param name="component">The <see cref="IComponent"/>.</param>
/// <returns>The assigned identifier for the <see cref="IComponent"/>.</returns>
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;
}
}
/// <summary>
/// Updates the visible UI to display the supplied <paramref name="renderTree"/>
/// at the location corresponding to the <paramref name="componentId"/>.
/// </summary>
/// <param name="componentId">The identifier for the updated <see cref="IComponent"/>.</param>
/// <param name="renderTree">The updated render tree to be displayed.</param>
internal protected abstract void UpdateDisplay(int componentId, ArraySegment<RenderTreeNode> renderTree);
/// <summary>
/// Updates the rendered state of the specified <see cref="IComponent"/>.
/// </summary>
/// <param name="component">The <see cref="IComponent"/>.</param>
protected void RenderComponent(IComponent component)
=> GetRequiredComponentState(component).Render();
/// <summary>
/// Updates the rendered state of the specified <see cref="IComponent"/>.
/// </summary>
/// <param name="componentId">The identifier of the <see cref="IComponent"/> to render.</param>
protected void RenderComponent(int componentId)
=> GetRequiredComponentState(componentId).Render();
/// <summary>
/// Notifies the specified component that an event has occurred.
/// </summary>
/// <param name="componentId">The unique identifier for the component within the scope of this <see cref="Renderer"/>.</param>
/// <param name="renderTreeIndex">The index into the component's current render tree that specifies which event handler to invoke.</param>
/// <param name="eventArgs">Arguments to be passed to the event handler.</param>
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.");
}
}

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Blazor.Browser
namespace Microsoft.Blazor.Rendering
{
internal class WeakValueDictionary<TKey, TValue> where TValue : class
{

View File

@ -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<InvalidOperationException>(() =>
@ -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<InvalidOperationException>(() =>
@ -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<InvalidOperationException>(() =>
@ -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<InvalidOperationException>(() =>
@ -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: <parent>
@ -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<RenderTreeNode> renderTree)
=> throw new NotImplementedException();
}
}
}

View File

@ -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);
}
}
}