Initial basic component rendering. Does not yet do any tree diffing. APIs will change.
This commit is contained in:
parent
29e0d4629b
commit
5453b58f31
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
<title>Sample Blazor app</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello</h1>
|
||||
<app>Loading...</app>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -4,4 +4,8 @@
|
|||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Blazor\Microsoft.Blazor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue