Initial basic component rendering. Does not yet do any tree diffing. APIs will change.

This commit is contained in:
Steve Sanderson 2018-01-04 14:11:37 +00:00
parent 29e0d4629b
commit 5453b58f31
14 changed files with 360 additions and 6 deletions

View File

@ -13,6 +13,7 @@
<Import Project="..\..\src\Microsoft.Blazor.Build\ReferenceFromSource.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Blazor.Browser\Microsoft.Blazor.Browser.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Blazor\Microsoft.Blazor.csproj" />
</ItemGroup>
</Project>

View File

@ -2,6 +2,8 @@
// 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.Components;
using Microsoft.Blazor.UITree;
namespace HostedInAspNet.Client
{
@ -9,7 +11,31 @@ namespace HostedInAspNet.Client
{
static void Main(string[] args)
{
new Renderer();
// Temporarily render this test component until there's a proper mechanism
// for testing this.
Renderer.Render(new MyComponent(), "app");
}
}
internal class MyComponent : IComponent
{
public void Render(UITreeBuilder builder)
{
builder.OpenElement("h1");
builder.AddText("Hello from UITree");
builder.CloseElement();
builder.OpenElement("ul");
builder.OpenElement("li");
builder.AddText("First item");
builder.CloseElement();
builder.OpenElement("li");
builder.AddText("Second item");
builder.CloseElement();
builder.CloseElement();
}
}
}

View File

@ -5,6 +5,6 @@
<title>Sample Blazor app</title>
</head>
<body>
<h1>Hello</h1>
<app>Loading...</app>
</body>
</html>

View File

@ -1,5 +1,6 @@
import { platform } from './Environment';
import { getAssemblyNameFromUrl } from './Platform/DotNet';
import './Rendering/Renderer';
async function boot() {
// Read startup config from the <script> element that's importing this file

View File

@ -1,4 +1,4 @@
import { MethodHandle, System_Object, System_String, Platform } from '../Platform';
import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform';
import { getAssemblyNameFromUrl } from '../DotNet';
let assembly_load: (assemblyName: string) => number;
@ -93,7 +93,25 @@ export const monoPlatform: Platform = {
toDotNetString: function toDotNetString(jsString: string): System_String {
return mono_string(jsString);
}
},
getArrayLength: function getArrayLength(array: System_Array): number {
return Module.getValue(getArrayDataPointer(array), 'i32');
},
getArrayEntryPtr: function getArrayEntryPtr(array: System_Array, index: number, itemSize: number): Pointer {
// First byte is array length, followed by entries
const address = getArrayDataPointer(array) + 4 + index * itemSize;
return address as any as Pointer;
},
readHeapInt32: function readHeapInt32(address: Pointer, offset?: number): number {
return Module.getValue((address as any as number) + (offset || 0), 'i32');
},
readHeapObject: function readHeapObject(address: Pointer, offset?: number): System_Object {
return monoPlatform.readHeapInt32(address, offset) as any as System_Object;
},
};
function addScriptTagsToDocument() {
@ -162,3 +180,7 @@ function asyncLoad(url, onload, onerror) {
xhr.onerror = onerror;
xhr.send(null);
}
function getArrayDataPointer(array: System_Array): number {
return <number><any>array + 12; // First byte from here is length, then following bytes are entries
}

View File

@ -7,6 +7,12 @@
toJavaScriptString(dotNetString: System_String): string;
toDotNetString(javaScriptString: string): System_String;
getArrayLength(array: System_Array): number;
getArrayEntryPtr(array: System_Array, index: number, itemSize: number): Pointer;
readHeapInt32(address: Pointer, offset?: number): number;
readHeapObject(address: Pointer, offset?: number): System_Object;
}
// We don't actually instantiate any of these at runtime. For perf it's preferable to
@ -15,3 +21,5 @@
export interface MethodHandle { MethodHandle__DO_NOT_IMPLEMENT: any };
export interface System_Object { System_Object__DO_NOT_IMPLEMENT: any };
export interface System_String extends System_Object { System_String__DO_NOT_IMPLEMENT: any }
export interface System_Array extends System_Object { System_Array__DO_NOT_IMPLEMENT: any }
export interface Pointer { Pointer__DO_NOT_IMPLEMENT: any }

View File

@ -0,0 +1,68 @@
import { registerFunction } from '../RegisteredFunction';
import { System_String, System_Array } from '../Platform/Platform';
import { platform } from '../Environment';
import { getTreeNodePtr, uiTreeNode, NodeType, UITreeNodePointer } from './UITreeNode';
registerFunction('_blazorRender', renderUITree);
function renderUITree(elementSelector: System_String, tree: System_Array, treeLength: number) {
const elementSelectorJs = platform.toJavaScriptString(elementSelector);
const element = document.querySelector(elementSelectorJs);
if (!element) {
throw new Error(`Could not find any element matching selector '${ elementSelectorJs }'.`);
}
clearElement(element);
insertNodeRange(element, tree, 0, treeLength - 1);
}
function insertNodeRange(intoDomElement: Element, tree: System_Array, startIndex: number, endIndex: number) {
for (let index = startIndex; index <= endIndex; index++) {
const node = getTreeNodePtr(tree, index);
insertNode(intoDomElement, tree, node, index);
// Skip over any descendants, since they are already dealt with recursively
const descendantsEndIndex = uiTreeNode.descendantsEndIndex(node);
if (descendantsEndIndex > 0) {
index = descendantsEndIndex;
}
}
}
function insertNode(intoDomElement: Element, tree: System_Array, node: UITreeNodePointer, nodeIndex: number) {
const nodeType = uiTreeNode.nodeType(node);
switch (nodeType) {
case NodeType.element:
insertElement(intoDomElement, tree, node, nodeIndex);
break;
case NodeType.text:
insertText(intoDomElement, node);
break;
default:
const unknownType: never = nodeType; // Compile-time verification that the switch was exhaustive
throw new Error(`Unknown node type: ${ unknownType }`);
}
}
function insertElement(intoDomElement: Element, tree: System_Array, elementNode: UITreeNodePointer, elementNodeIndex: number) {
const tagName = uiTreeNode.elementName(elementNode);
const newDomElement = document.createElement(tagName);
intoDomElement.appendChild(newDomElement);
// Recursively insert children
const descendantsEndIndex = uiTreeNode.descendantsEndIndex(elementNode);
insertNodeRange(newDomElement, tree, elementNodeIndex + 1, descendantsEndIndex);
}
function insertText(intoDomElement: Element, textNode: UITreeNodePointer) {
const textContent = uiTreeNode.textContent(textNode);
const newDomTextNode = document.createTextNode(textContent);
intoDomElement.appendChild(newDomTextNode);
}
function clearElement(element: Element) {
let childNode: Node;
while (childNode = element.firstChild) {
element.removeChild(childNode);
}
}

View File

@ -0,0 +1,38 @@
import { System_String, System_Array, Pointer } from '../Platform/Platform';
import { platform } from '../Environment';
const uiTreeNodeStructLength = 16;
// 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
// know how to read property values from those structs.
export function getTreeNodePtr(uiTreeEntries: System_Array, index: number): UITreeNodePointer {
return platform.getArrayEntryPtr(uiTreeEntries, index, uiTreeNodeStructLength) as UITreeNodePointer;
}
export const uiTreeNode = {
// The properties and memory layout must be kept in sync with the .NET equivalent in UITreeNode.cs
nodeType: (node: UITreeNodePointer) => _readInt32Property(node, 0) as NodeType,
elementName: (node: UITreeNodePointer) => _readStringProperty(node, 4),
descendantsEndIndex: (node: UITreeNodePointer) => _readInt32Property(node, 8) as NodeType,
textContent: (node: UITreeNodePointer) => _readStringProperty(node, 12),
};
export enum NodeType {
// The values must be kept in sync with the .NET equivalent in UITreeNodeType.cs
element = 1,
text = 2
}
function _readInt32Property(baseAddress: Pointer, offsetBytes: number) {
return platform.readHeapInt32(baseAddress, offsetBytes);
}
function _readStringProperty(baseAddress: Pointer, offsetBytes: number) {
var managedString = platform.readHeapObject(baseAddress, offsetBytes) as System_String;
return platform.toJavaScriptString(managedString);
}
// Nominal type to ensure only valid pointers are passed to the uiTreeNode functions.
// At runtime the values are just numbers.
export interface UITreeNodePointer extends Pointer { UITreeNodePointer__DO_NOT_IMPLEMENT: any }

View File

@ -4,4 +4,8 @@
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Blazor\Microsoft.Blazor.csproj" />
</ItemGroup>
</Project>

View File

@ -1,12 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Blazor.Browser.Interop;
using Microsoft.Blazor.Components;
using Microsoft.Blazor.UITree;
namespace Microsoft.Blazor.Browser
{
public class Renderer
public static class Renderer
{
public Renderer()
public static void Render(IComponent component, string elementSelector)
{
var builder = new UITreeBuilder();
component.Render(builder);
var tree = builder.GetNodes();
RegisteredFunction.InvokeUnmarshalled<string, UITreeNode[], int, object>(
"_blazorRender", elementSelector, tree.Array, tree.Count);
}
}
}

View File

@ -0,0 +1,19 @@
// 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.UITree;
namespace Microsoft.Blazor.Components
{
/// <summary>
/// Represents a UI component.
/// </summary>
public interface IComponent
{
/// <summary>
/// Renders the component.
/// </summary>
/// <param name="builder">A <see cref="UITreeBuilder"/> to which the rendered nodes should be appended.</param>
void Render(UITreeBuilder builder);
}
}

View File

@ -0,0 +1,84 @@
// 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;
using System.Collections.Generic;
namespace Microsoft.Blazor.UITree
{
/// <summary>
/// Provides methods for building a collection of <see cref="UITreeNode"/> entries.
/// </summary>
public class UITreeBuilder
{
private const int MinBufferLength = 10;
private UITreeNode[] _entries = new UITreeNode[100];
private int _entriesInUse = 0;
private Stack<int> _openElementIndices = new Stack<int>();
/// <summary>
/// Appends a node representing an element, i.e., a container for other nodes.
/// In order for the <see cref="UITreeBuilder"/> state to be valid, you must
/// also call <see cref="CloseElement"/> immediately after appending the
/// new element's child nodes.
/// </summary>
/// <param name="elementName">A value representing the type of the element.</param>
public void OpenElement(string elementName)
{
_openElementIndices.Push(_entriesInUse);
Append(UITreeNode.Element(elementName));
}
/// <summary>
/// Marks a previously appended element node as closed. Calls to this method
/// must be balanced with calls to <see cref="OpenElement(string)"/>.
/// </summary>
public void CloseElement()
{
var indexOfEntryBeingClosed = _openElementIndices.Pop();
_entries[indexOfEntryBeingClosed].CloseElement(_entriesInUse - 1);
}
/// <summary>
/// Appends a node representing text content.
/// </summary>
/// <param name="textContent">Content for the new text node.</param>
public void AddText(string textContent)
=> Append(UITreeNode.Text(textContent));
/// <summary>
/// Clears the builder.
/// </summary>
public void Clear()
{
// If the previous usage of the buffer showed that we have allocated
// much more space than needed, free up the excess memory
var shrinkToLength = Math.Max(MinBufferLength, _entries.Length / 2);
if (_entriesInUse < shrinkToLength)
{
Array.Resize(ref _entries, shrinkToLength);
}
_entriesInUse = 0;
_openElementIndices.Clear();
}
/// <summary>
/// Returns the <see cref="UITreeNode"/> values that have been appended.
/// The return value's <see cref="ArraySegment{T}.Offset"/> is always zero.
/// </summary>
/// <returns>An array segment of <see cref="UITreeNode"/> values.</returns>
public ArraySegment<UITreeNode> GetNodes() =>
new ArraySegment<UITreeNode>(_entries, 0, _entriesInUse);
private void Append(UITreeNode node)
{
if (_entriesInUse == _entries.Length)
{
Array.Resize(ref _entries, _entries.Length * 2);
}
_entries[_entriesInUse++] = node;
}
}
}

View File

@ -0,0 +1,52 @@
// 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.
namespace Microsoft.Blazor.UITree
{
/// <summary>
/// Represents an entry in a tree of user interface (UI) items.
/// </summary>
public struct UITreeNode
{
/// <summary>
/// Describes the type of this node.
/// </summary>
public UITreeNodeType NodeType { get; private set; }
/// <summary>
/// If the <see cref="NodeType"/> property equals <see cref="UITreeNodeType.Element"/>,
/// gets a name representing the type of the element. Otherwise, the value is <see langword="null"/>.
/// </summary>
public string ElementName { get; private set; }
/// <summary>
/// If the <see cref="NodeType"/> property equals <see cref="UITreeNodeType.Element"/>,
/// gets the index of the final descendant node in the tree. The value is
/// zero if the node is of a different type, or if it has not yet been closed.
/// </summary>
public int ElementDescendantsEndIndex { get; private set; }
/// <summary>
/// If the <see cref="NodeType"/> property equals <see cref="UITreeNodeType.Text"/>,
/// gets the content of the text node. Otherwise, the value is <see langword="null"/>.
/// </summary>
public string TextContent { get; private set; }
internal static UITreeNode Element(string elementName) => new UITreeNode
{
NodeType = UITreeNodeType.Element,
ElementName = elementName,
};
internal static UITreeNode Text(string textContent) => new UITreeNode
{
NodeType = UITreeNodeType.Text,
TextContent = textContent,
};
internal void CloseElement(int descendantsEndIndex)
{
ElementDescendantsEndIndex = descendantsEndIndex;
}
}
}

View File

@ -0,0 +1,21 @@
// 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.
namespace Microsoft.Blazor.UITree
{
/// <summary>
/// Describes the type of a <see cref="UITreeNode"/>.
/// </summary>
public enum UITreeNodeType: int
{
/// <summary>
/// Represents a container for other nodes.
/// </summary>
Element = 1,
/// <summary>
/// Represents text content.
/// </summary>
Text = 2,
}
}