Refactor most of the rendering logic into Microsoft.Blazor, keeping only browser-specific parts in Microsoft.Blazor.Browser
This commit is contained in:
parent
674024ed61
commit
6585667ce6
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue