Basic parameterless events support (e.g., button click)

This commit is contained in:
Steve Sanderson 2018-01-05 22:09:05 +00:00
parent f7cb54121b
commit 23f7120b75
12 changed files with 315 additions and 57 deletions

View File

@ -1,7 +1,8 @@
import { registerFunction } from '../RegisteredFunction'; import { registerFunction } from '../RegisteredFunction';
import { System_String, System_Array } from '../Platform/Platform'; import { System_Object, System_String, System_Array, MethodHandle } from '../Platform/Platform';
import { platform } from '../Environment'; import { platform } from '../Environment';
import { getTreeNodePtr, uiTreeNode, NodeType, UITreeNodePointer } from './UITreeNode'; import { getTreeNodePtr, uiTreeNode, NodeType, UITreeNodePointer } from './UITreeNode';
let raiseEventMethod: MethodHandle;
// TODO: Instead of associating components to parent elements, associate them with a // 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 // start/end node, so that components don't have to be enclosed in a wrapper
@ -20,8 +21,6 @@ function attachComponentToElement(elementSelector: System_String, componentId: S
throw new Error(`Could not find any element matching selector '${elementSelectorJs}'.`); throw new Error(`Could not find any element matching selector '${elementSelectorJs}'.`);
} }
clearElement(element);
const componentIdJs = platform.toJavaScriptString(componentId); const componentIdJs = platform.toJavaScriptString(componentId);
componentIdToParentElement[componentIdJs] = element; componentIdToParentElement[componentIdJs] = element;
} }
@ -33,13 +32,14 @@ function renderUITree(componentId: System_String, tree: System_Array, treeLength
throw new Error(`No element is currently associated with component ${componentIdJs}`); throw new Error(`No element is currently associated with component ${componentIdJs}`);
} }
insertNodeRange(element, tree, 0, treeLength - 1); clearElement(element);
insertNodeRange(componentIdJs, element, tree, 0, treeLength - 1);
} }
function insertNodeRange(intoDomElement: Element, tree: System_Array, startIndex: number, endIndex: number) { function insertNodeRange(componentId: string, intoDomElement: Element, tree: System_Array, startIndex: number, endIndex: number) {
for (let index = startIndex; index <= endIndex; index++) { for (let index = startIndex; index <= endIndex; index++) {
const node = getTreeNodePtr(tree, index); const node = getTreeNodePtr(tree, index);
insertNode(intoDomElement, tree, node, index); insertNode(componentId, intoDomElement, tree, node, index);
// Skip over any descendants, since they are already dealt with recursively // Skip over any descendants, since they are already dealt with recursively
const descendantsEndIndex = uiTreeNode.descendantsEndIndex(node); const descendantsEndIndex = uiTreeNode.descendantsEndIndex(node);
@ -49,11 +49,11 @@ function insertNodeRange(intoDomElement: Element, tree: System_Array, startIndex
} }
} }
function insertNode(intoDomElement: Element, tree: System_Array, node: UITreeNodePointer, nodeIndex: number) { function insertNode(componentId: string, intoDomElement: Element, tree: System_Array, node: UITreeNodePointer, nodeIndex: number) {
const nodeType = uiTreeNode.nodeType(node); const nodeType = uiTreeNode.nodeType(node);
switch (nodeType) { switch (nodeType) {
case NodeType.element: case NodeType.element:
insertElement(intoDomElement, tree, node, nodeIndex); insertElement(componentId, intoDomElement, tree, node, nodeIndex);
break; break;
case NodeType.text: case NodeType.text:
insertText(intoDomElement, node); insertText(intoDomElement, node);
@ -66,7 +66,7 @@ function insertNode(intoDomElement: Element, tree: System_Array, node: UITreeNod
} }
} }
function insertElement(intoDomElement: Element, tree: System_Array, elementNode: UITreeNodePointer, elementNodeIndex: number) { function insertElement(componentId: string, intoDomElement: Element, tree: System_Array, elementNode: UITreeNodePointer, elementNodeIndex: number) {
const tagName = uiTreeNode.elementName(elementNode); const tagName = uiTreeNode.elementName(elementNode);
const newDomElement = document.createElement(tagName); const newDomElement = document.createElement(tagName);
intoDomElement.appendChild(newDomElement); intoDomElement.appendChild(newDomElement);
@ -76,21 +76,46 @@ function insertElement(intoDomElement: Element, tree: System_Array, elementNode:
for (let descendantIndex = elementNodeIndex + 1; descendantIndex <= descendantsEndIndex; descendantIndex++) { for (let descendantIndex = elementNodeIndex + 1; descendantIndex <= descendantsEndIndex; descendantIndex++) {
const descendantNode = getTreeNodePtr(tree, descendantIndex); const descendantNode = getTreeNodePtr(tree, descendantIndex);
if (uiTreeNode.nodeType(descendantNode) === NodeType.attribute) { if (uiTreeNode.nodeType(descendantNode) === NodeType.attribute) {
applyAttribute(newDomElement, descendantNode); applyAttribute(componentId, newDomElement, descendantNode, descendantIndex);
} else { } else {
// As soon as we see a non-attribute child, all the subsequent child nodes are // 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 // not attributes, so bail out and insert the remnants recursively
insertNodeRange(newDomElement, tree, descendantIndex, descendantsEndIndex); insertNodeRange(componentId, newDomElement, tree, descendantIndex, descendantsEndIndex);
break; break;
} }
} }
} }
function applyAttribute(toDomElement: Element, attributeNode: UITreeNodePointer) { function applyAttribute(componentId: string, toDomElement: Element, attributeNode: UITreeNodePointer, attributeNodeIndex: number) {
toDomElement.setAttribute( const attributeName = uiTreeNode.attributeName(attributeNode);
uiTreeNode.attributeName(attributeNode),
uiTreeNode.attributeValue(attributeNode) switch (attributeName) {
); case 'onclick':
toDomElement.addEventListener('click', () => raiseEvent(componentId, attributeNodeIndex, 'click'));
break;
default:
// Treat as a regular string-valued attribute
toDomElement.setAttribute(
attributeName,
uiTreeNode.attributeValue(attributeNode)
);
break;
}
}
function raiseEvent(componentId: string, uiTreeNodeIndex: number, eventName: string) {
if (!raiseEventMethod) {
raiseEventMethod = platform.findMethod(
'Microsoft.Blazor.Browser', 'Microsoft.Blazor.Browser', 'Events', 'RaiseEvent'
);
}
// TODO: Find a way of passing the uiTreeNodeIndex as a System.Int32, possibly boxing
// it first if necessary. Until then we have to send it as a string.
platform.callMethod(raiseEventMethod, null, [
platform.toDotNetString(componentId),
platform.toDotNetString(uiTreeNodeIndex.toString())
]);
} }
function insertText(intoDomElement: Element, textNode: UITreeNodePointer) { function insertText(intoDomElement: Element, textNode: UITreeNodePointer) {

View File

@ -1,6 +1,6 @@
import { System_String, System_Array, Pointer } from '../Platform/Platform'; import { System_String, System_Array, Pointer } from '../Platform/Platform';
import { platform } from '../Environment'; import { platform } from '../Environment';
const uiTreeNodeStructLength = 24; const uiTreeNodeStructLength = 28;
// To minimise GC pressure, instead of instantiating a JS object to represent each tree node, // 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 // we work in terms of pointers to the structs on the .NET heap, and use static functions that

View File

@ -3,7 +3,6 @@
using Microsoft.Blazor.Browser.Interop; using Microsoft.Blazor.Browser.Interop;
using Microsoft.Blazor.Components; using Microsoft.Blazor.Components;
using Microsoft.Blazor.UITree;
namespace Microsoft.Blazor.Browser namespace Microsoft.Blazor.Browser
{ {
@ -26,17 +25,7 @@ namespace Microsoft.Blazor.Browser
RegisteredFunction.InvokeUnmarshalled<string, string, object>( RegisteredFunction.InvokeUnmarshalled<string, string, object>(
"_blazorAttachComponentToElement", elementSelector, renderState.DOMComponentId); "_blazorAttachComponentToElement", elementSelector, renderState.DOMComponentId);
RefreshComponentInDOM(renderState); renderState.RenderToDOM();
}
private static void RefreshComponentInDOM(DOMComponentRenderState renderState)
{
var tree = renderState.UpdateRender();
RegisteredFunction.InvokeUnmarshalled<string, UITreeNode[], int, object>(
"_blazorRender",
renderState.DOMComponentId,
tree.Array,
tree.Count);
} }
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Components;
using Microsoft.Blazor.UITree; using Microsoft.Blazor.UITree;
using System; using System;
@ -14,8 +15,16 @@ namespace Microsoft.Blazor.Browser
/// </summary> /// </summary>
internal class DOMComponentRenderState internal class DOMComponentRenderState
{ {
private static ConditionalWeakTable<IComponent, DOMComponentRenderState> _stateInstances // 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>(); = new ConditionalWeakTable<IComponent, DOMComponentRenderState>();
private static WeakValueDictionary<string, DOMComponentRenderState> _renderStatesByComponentId
= new WeakValueDictionary<string, DOMComponentRenderState>();
private static long _nextDOMComponentId = 0; private static long _nextDOMComponentId = 0;
private readonly UITreeBuilder _uITreeBuilder; // TODO: Maintain two, so we can diff successive renders private readonly UITreeBuilder _uITreeBuilder; // TODO: Maintain two, so we can diff successive renders
@ -26,16 +35,16 @@ namespace Microsoft.Blazor.Browser
private DOMComponentRenderState(string componentId, IComponent component) private DOMComponentRenderState(string componentId, IComponent component)
{ {
DOMComponentId = DOMComponentId; DOMComponentId = componentId;
Component = component; Component = component;
_uITreeBuilder = new UITreeBuilder(); _uITreeBuilder = new UITreeBuilder();
} }
public static DOMComponentRenderState GetOrCreate(IComponent component) public static DOMComponentRenderState GetOrCreate(IComponent component)
{ {
lock (_stateInstances) lock (_renderStatesByComponent)
{ {
if (_stateInstances.TryGetValue(component, out var existingState)) if (_renderStatesByComponent.TryGetValue(component, out var existingState))
{ {
return existingState; return existingState;
} }
@ -43,13 +52,19 @@ namespace Microsoft.Blazor.Browser
{ {
var newId = (_nextDOMComponentId++).ToString(); var newId = (_nextDOMComponentId++).ToString();
var newState = new DOMComponentRenderState(newId, component); var newState = new DOMComponentRenderState(newId, component);
_stateInstances.Add(component, newState); _renderStatesByComponent.Add(component, newState);
_renderStatesByComponentId.Add(newId, newState);
return newState; return newState;
} }
} }
} }
public ArraySegment<UITreeNode> UpdateRender() 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<UITreeNode> UpdateRender()
{ {
_uITreeBuilder.Clear(); _uITreeBuilder.Clear();
Component.BuildUITree(_uITreeBuilder); Component.BuildUITree(_uITreeBuilder);
@ -57,5 +72,28 @@ namespace Microsoft.Blazor.Browser
// TODO: Change this to return a diff between the previous render result and this new one // TODO: Change this to return a diff between the previous render result and this new one
return _uITreeBuilder.GetNodes(); return _uITreeBuilder.GetNodes();
} }
public void RaiseEvent(int uiTreeNodeIndex)
{
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(UITreeNode)} at index {uiTreeNodeIndex} does not have any {nameof(UITreeNode.AttributeEventHandlerValue)}.");
}
eventHandler.Invoke();
RenderToDOM();
}
public void RenderToDOM()
{
var tree = UpdateRender();
RegisteredFunction.InvokeUnmarshalled<string, UITreeNode[], int, object>(
"_blazorRender",
DOMComponentId,
tree.Array,
tree.Count);
}
} }
} }

View File

@ -0,0 +1,18 @@
// 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.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)
{
// 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);
renderState.RaiseEvent(int.Parse(uiTreeNodeIndex));
}
}
}

View File

@ -0,0 +1,61 @@
// 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;
using System.Linq;
namespace Microsoft.Blazor.Browser
{
internal class WeakValueDictionary<TKey, TValue> where TValue : class
{
private IDictionary<TKey, WeakReference<TValue>> _store
= new Dictionary<TKey, WeakReference<TValue>>();
private int _cullThreshold = 10;
public bool TryGetValue(TKey key, out TValue value)
{
if (_store.TryGetValue(key, out var existingWeakRef))
{
if (existingWeakRef.TryGetTarget(out value))
{
return true;
}
// Since we know it's not there, we might as well drop the entry now
_store.Remove(key);
}
value = default(TValue);
return false;
}
public void Add(TKey key, TValue value)
{
if (_store.TryGetValue(key, out _))
{
throw new ArgumentException($"The given key was already present in the {nameof(WeakValueDictionary<TKey, TValue>)}. Key: {key}");
}
_store[key] = new WeakReference<TValue>(value);
CullIfApplicable();
}
private void CullIfApplicable()
{
if (_store.Count > _cullThreshold)
{
var itemsToRemove = _store.Where(x => !x.Value.TryGetTarget(out _)).ToList();
foreach (var itemToRemove in itemsToRemove)
{
_store.Remove(itemToRemove.Key);
}
if (_store.Count > (_cullThreshold / 2))
{
_cullThreshold *= 2;
}
}
}
}
}

View File

@ -0,0 +1,10 @@
// 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>
/// Handles an event raised for a <see cref="UITreeNode"/>.
/// </summary>
public delegate void UIEventHandler();
}

View File

@ -48,18 +48,32 @@ namespace Microsoft.Blazor.UITree
=> Append(UITreeNode.Text(textContent)); => Append(UITreeNode.Text(textContent));
/// <summary> /// <summary>
/// Appends a node representing an attribute. The attribute is associated /// Appends a node representing a string-valued attribute.
/// with the most recently added element. /// The attribute is associated with the most recently added element.
/// </summary> /// </summary>
/// <param name="name">The name of the attribute.</param> /// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param> /// <param name="value">The value of the attribute.</param>
public void AddAttribute(string name, string value) public void AddAttribute(string name, string value)
{ {
if (_lastNonAttributeNodeType == UITreeNodeType.Element) AssertCanAddAttribute();
{ Append(UITreeNode.Attribute(name, value));
Append(UITreeNode.Attribute(name, value)); }
}
else /// <summary>
/// Appends a node representing an <see cref="UIEventHandler"/>-valued attribute.
/// The attribute is associated with the most recently added element.
/// </summary>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(string name, UIEventHandler value)
{
AssertCanAddAttribute();
Append(UITreeNode.Attribute(name, value));
}
private void AssertCanAddAttribute()
{
if (_lastNonAttributeNodeType != UITreeNodeType.Element)
{ {
throw new InvalidOperationException($"Attributes may only be added immediately after nodes of type {UITreeNodeType.Element}"); throw new InvalidOperationException($"Attributes may only be added immediately after nodes of type {UITreeNodeType.Element}");
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.Blazor.UITree namespace Microsoft.Blazor.UITree
{ {
// TODO: Consider coalescing properties of compatible types that don't need to be // TODO: Consider coalescing properties of compatible types that don't need to be
@ -48,6 +50,12 @@ namespace Microsoft.Blazor.UITree
/// </summary> /// </summary>
public string AttributeValue { get; private set; } public string AttributeValue { get; private set; }
/// <summary>
/// If the <see cref="NodeType"/> property equals <see cref="UITreeNodeType.Attribute"/>,
/// gets the attribute's event handle, if any. Otherwise, the value is <see langword="null"/>.
/// </summary>
public UIEventHandler AttributeEventHandlerValue { get; private set; }
internal static UITreeNode Element(string elementName) => new UITreeNode internal static UITreeNode Element(string elementName) => new UITreeNode
{ {
NodeType = UITreeNodeType.Element, NodeType = UITreeNodeType.Element,
@ -67,6 +75,13 @@ namespace Microsoft.Blazor.UITree
AttributeValue = value AttributeValue = value
}; };
internal static UITreeNode Attribute(string name, UIEventHandler value) => new UITreeNode
{
NodeType = UITreeNodeType.Attribute,
AttributeName = name,
AttributeEventHandlerValue = value
};
internal void CloseElement(int descendantsEndIndex) internal void CloseElement(int descendantsEndIndex)
{ {
ElementDescendantsEndIndex = descendantsEndIndex; ElementDescendantsEndIndex = descendantsEndIndex;

View File

@ -3,6 +3,7 @@
using System; using System;
using BasicTestApp; using BasicTestApp;
using Microsoft.Blazor.Components;
using Microsoft.Blazor.E2ETest.Infrastructure; using Microsoft.Blazor.E2ETest.Infrastructure;
using Microsoft.Blazor.E2ETest.Infrastructure.ServerFixtures; using Microsoft.Blazor.E2ETest.Infrastructure.ServerFixtures;
using OpenQA.Selenium; using OpenQA.Selenium;
@ -17,43 +18,55 @@ namespace Microsoft.Blazor.E2ETest.Tests
public ComponentRenderingTest(BrowserFixture browserFixture, DevHostServerFixture<Program> serverFixture) public ComponentRenderingTest(BrowserFixture browserFixture, DevHostServerFixture<Program> serverFixture)
: base(browserFixture, serverFixture) : base(browserFixture, serverFixture)
{ {
Navigate("/", noReload: true);
} }
[Fact] [Fact]
public void BasicTestAppCanBeServed() public void BasicTestAppCanBeServed()
{ {
Navigate("/", noReload: true);
Assert.Equal("Basic test app", Browser.Title); Assert.Equal("Basic test app", Browser.Title);
} }
[Fact] [Fact]
public void CanRenderTextOnlyComponent() public void CanRenderTextOnlyComponent()
{ {
Navigate("/", noReload: true); var appElement = MountTestComponent<TextOnlyComponent>();
MountTestComponent("BasicTestApp.TextOnlyComponent");
var appElement = Browser.FindElement(By.TagName("app"));
Assert.Equal("Hello from TextOnlyComponent", appElement.Text); Assert.Equal("Hello from TextOnlyComponent", appElement.Text);
} }
[Fact] [Fact]
public void CanRenderComponentWithAttributes() public void CanRenderComponentWithAttributes()
{ {
Navigate("/", noReload: true); var appElement = MountTestComponent<RedTextComponent>();
MountTestComponent("BasicTestApp.RedTextComponent");
var appElement = Browser.FindElement(By.TagName("app"));
var styledElement = appElement.FindElement(By.TagName("h1")); var styledElement = appElement.FindElement(By.TagName("h1"));
Assert.Equal("Hello, world!", styledElement.Text); Assert.Equal("Hello, world!", styledElement.Text);
Assert.Equal("color: red;", styledElement.GetAttribute("style")); Assert.Equal("color: red;", styledElement.GetAttribute("style"));
Assert.Equal("somevalue", styledElement.GetAttribute("customattribute")); Assert.Equal("somevalue", styledElement.GetAttribute("customattribute"));
} }
private void MountTestComponent(string componentTypeName) [Fact]
public void CanTriggerEvents()
{ {
var appElement = MountTestComponent<CounterComponent>();
Assert.Equal(
"Current count: 0",
appElement.FindElement(By.TagName("p")).Text);
appElement.FindElement(By.TagName("button")).Click();
Assert.Equal(
"Current count: 1",
appElement.FindElement(By.TagName("p")).Text);
}
private IWebElement MountTestComponent<TComponent>() where TComponent: IComponent
{
var componentTypeName = typeof(TComponent).FullName;
WaitUntilDotNetRunningInBrowser(); WaitUntilDotNetRunningInBrowser();
((IJavaScriptExecutor)Browser).ExecuteScript( ((IJavaScriptExecutor)Browser).ExecuteScript(
$"mountTestComponent('{componentTypeName}')"); $"mountTestComponent('{componentTypeName}')");
return Browser.FindElement(By.TagName("app"));
} }
private void WaitUntilDotNetRunningInBrowser() private void WaitUntilDotNetRunningInBrowser()

View File

@ -136,13 +136,14 @@ namespace Microsoft.Blazor.Test
{ {
// Arrange // Arrange
var builder = new UITreeBuilder(); var builder = new UITreeBuilder();
UIEventHandler eventHandler = () => { };
// Act // Act
builder.OpenElement("myelement"); // 0: <myelement builder.OpenElement("myelement"); // 0: <myelement
builder.AddAttribute("attribute1", "value 1"); // 1: attribute1="value 1" builder.AddAttribute("attribute1", "value 1"); // 1: attribute1="value 1"
builder.AddAttribute("attribute2", "value 2"); // 2: attribute2="value 2"> builder.AddAttribute("attribute2", "value 2"); // 2: attribute2="value 2">
builder.OpenElement("child"); // 3: <child builder.OpenElement("child"); // 3: <child
builder.AddAttribute("attribute1", "child value"); // 4: attribute1="child value"> builder.AddAttribute("childevent", eventHandler); // 4: childevent=eventHandler>
builder.AddText("some text"); // 5: some text builder.AddText("some text"); // 5: some text
builder.CloseElement(); // </child> builder.CloseElement(); // </child>
builder.CloseElement(); // </myelement> builder.CloseElement(); // </myelement>
@ -153,12 +154,12 @@ namespace Microsoft.Blazor.Test
node => AssertAttribute(node, "attribute1", "value 1"), node => AssertAttribute(node, "attribute1", "value 1"),
node => AssertAttribute(node, "attribute2", "value 2"), node => AssertAttribute(node, "attribute2", "value 2"),
node => AssertElement(node, "child", 5), node => AssertElement(node, "child", 5),
node => AssertAttribute(node, "attribute1", "child value"), node => AssertAttribute(node, "childevent", eventHandler),
node => AssertText(node, "some text")); node => AssertText(node, "some text"));
} }
[Fact] [Fact]
public void CannotAddAttributesAtRoot() public void CannotAddAttributeAtRoot()
{ {
// Arrange // Arrange
var builder = new UITreeBuilder(); var builder = new UITreeBuilder();
@ -171,7 +172,20 @@ namespace Microsoft.Blazor.Test
} }
[Fact] [Fact]
public void CannotAddAttributesToText() public void CannotAddEventHandlerAttributeAtRoot()
{
// Arrange
var builder = new UITreeBuilder();
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.AddAttribute("name", () => { });
});
}
[Fact]
public void CannotAddAttributeToText()
{ {
// Arrange // Arrange
var builder = new UITreeBuilder(); var builder = new UITreeBuilder();
@ -185,6 +199,21 @@ namespace Microsoft.Blazor.Test
}); });
} }
[Fact]
public void CannotAddEventHandlerAttributeToText()
{
// Arrange
var builder = new UITreeBuilder();
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.OpenElement("some element");
builder.AddText("hello");
builder.AddAttribute("name", () => { });
});
}
[Fact] [Fact]
public void CanClear() public void CanClear()
{ {
@ -216,11 +245,22 @@ namespace Microsoft.Blazor.Test
Assert.Equal(descendantsEndIndex, node.ElementDescendantsEndIndex); Assert.Equal(descendantsEndIndex, node.ElementDescendantsEndIndex);
} }
void AssertAttribute(UITreeNode node, string attributeName, string attributeValue) void AssertAttribute(UITreeNode node, string attributeName)
{ {
Assert.Equal(UITreeNodeType.Attribute, node.NodeType); Assert.Equal(UITreeNodeType.Attribute, node.NodeType);
Assert.Equal(attributeName, node.AttributeName); Assert.Equal(attributeName, node.AttributeName);
}
void AssertAttribute(UITreeNode node, string attributeName, string attributeValue)
{
AssertAttribute(node, attributeName);
Assert.Equal(attributeValue, node.AttributeValue); Assert.Equal(attributeValue, node.AttributeValue);
} }
void AssertAttribute(UITreeNode node, string attributeName, UIEventHandler attributeEventHandlerValue)
{
AssertAttribute(node, attributeName);
Assert.Equal(attributeEventHandlerValue, node.AttributeEventHandlerValue);
}
} }
} }

View File

@ -0,0 +1,35 @@
// 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.UITree;
namespace BasicTestApp
{
public class CounterComponent : IComponent
{
private int currentCount = 0;
public void BuildUITree(UITreeBuilder builder)
{
builder.OpenElement("h1");
builder.AddText("Counter");
builder.CloseElement();
builder.OpenElement("p");
builder.AddText("Current count: ");
builder.AddText(currentCount.ToString());
builder.CloseElement();
builder.OpenElement("button");
builder.AddAttribute("onclick", OnButtonClicked);
builder.AddText("Click me");
builder.CloseElement();
}
private void OnButtonClicked()
{
currentCount++;
}
}
}