Skip to content

Forms behavior clarifications #49340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b7ae702
Begin simplifying EditForm. All E2E tests pass so far.
SteveSandersonMS Jul 11, 2023
a00db37
Remove unnecessary name attribute from EditForm
SteveSandersonMS Jul 11, 2023
663c7d8
EditForm specifies handler via hidden field
SteveSandersonMS Jul 11, 2023
878f496
Eliminate unused binding context ID concept
SteveSandersonMS Jul 11, 2023
a2f41cb
Automatically emit hidden field for form handler name during SSR
SteveSandersonMS Jul 12, 2023
fb5d09b
Clear up error cases
SteveSandersonMS Jul 12, 2023
b224ab1
Decouple scope prefixing from EditForm so it works identically with p…
SteveSandersonMS Jul 12, 2023
075399e
Generate field identifiers when not in an edit context
SteveSandersonMS Jul 12, 2023
9890f66
Make specifying the form name on [SupplyParameterFromForm] optional. …
SteveSandersonMS Jul 12, 2023
24af917
Test updates
SteveSandersonMS Jul 12, 2023
cfa25e6
API tidy
SteveSandersonMS Jul 13, 2023
cc36e2d
Unit tests for scope/form matching rules
SteveSandersonMS Jul 13, 2023
bc59f78
Build fix
SteveSandersonMS Jul 13, 2023
6c4d607
Update E2E tests to remove ActionForm and use plain form instead
SteveSandersonMS Jul 13, 2023
10f1c46
More E2E test cases for plain form
SteveSandersonMS Jul 13, 2023
01f7c99
Fixing editable value sync
SteveSandersonMS Jul 13, 2023
6226b03
Better solution for editable value sync
SteveSandersonMS Jul 13, 2023
7043c17
Update .js files
SteveSandersonMS Jul 13, 2023
1a768ec
Return 400 for invalid handlers
SteveSandersonMS Jul 13, 2023
669c831
Require form to have nonempty handler
SteveSandersonMS Jul 13, 2023
df467c5
Reduce representation of named events to a single collection on the r…
SteveSandersonMS Jul 14, 2023
08ae762
Don't reject ambiguous forms until dispatching the event.
SteveSandersonMS Jul 14, 2023
e3dd2e7
Rename @onsubmit:name to @formname, and make EditForm consistent with…
SteveSandersonMS Jul 14, 2023
ad8d472
Better error messages
SteveSandersonMS Jul 14, 2023
774d014
Fix ordering of processing named submit events
SteveSandersonMS Jul 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/Components/Components/src/CascadingParameterState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Reflection;
using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using static Microsoft.AspNetCore.Internal.LinkerFlags;

namespace Microsoft.AspNetCore.Components;
Expand Down Expand Up @@ -43,7 +44,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
for (var infoIndex = 0; infoIndex < numInfos; infoIndex++)
{
ref var info = ref infos[infoIndex];
var supplier = GetMatchingCascadingValueSupplier(info, componentState);
var supplier = GetMatchingCascadingValueSupplier(info, componentState.Renderer, componentState.LogicalParentComponentState);
if (supplier != null)
{
// Although not all parameters might be matched, we know the maximum number
Expand All @@ -55,10 +56,10 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
return resultStates ?? (IReadOnlyList<CascadingParameterState>)Array.Empty<CascadingParameterState>();
}

private static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in CascadingParameterInfo info, ComponentState componentState)
internal static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in CascadingParameterInfo info, Renderer renderer, ComponentState? componentState)
{
// First scan up through the component hierarchy
var candidate = componentState.LogicalParentComponentState;
var candidate = componentState;
while (candidate is not null)
{
if (candidate.Component is ICascadingValueSupplier valueSupplier && valueSupplier.CanSupplyValue(info))
Expand All @@ -70,7 +71,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
}

// We got to the root and found no match, so now look at the providers registered in DI
foreach (var valueSupplier in componentState.Renderer.ServiceProviderCascadingValueSuppliers)
foreach (var valueSupplier in renderer.ServiceProviderCascadingValueSuppliers)
{
if (valueSupplier.CanSupplyValue(info))
{
Expand Down
21 changes: 12 additions & 9 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMo
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddNamedEvent(int sequence, string! eventType, string! assignedName) -> void
Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags.HasCallerSpecifiedRenderMode = 1 -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
Microsoft.AspNetCore.Components.RenderTree.NamedEvent
Microsoft.AspNetCore.Components.RenderTree.NamedEvent.AssignedName.get -> string!
Microsoft.AspNetCore.Components.RenderTree.NamedEvent.ComponentId.get -> int
Microsoft.AspNetCore.Components.RenderTree.NamedEvent.EventType.get -> string!
Microsoft.AspNetCore.Components.RenderTree.NamedEvent.FrameIndex.get -> int
Microsoft.AspNetCore.Components.RenderTree.NamedEvent.NamedEvent() -> void
Microsoft.AspNetCore.Components.RenderTree.NamedEvent.NamedEvent(int componentId, int frameIndex, string! eventType, string! assignedName) -> void
Microsoft.AspNetCore.Components.RenderTree.RenderBatch.AddedNamedEvents.get -> Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.NamedEvent>?
Microsoft.AspNetCore.Components.RenderTree.RenderBatch.RemovedNamedEvents.get -> Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.NamedEvent>?
Microsoft.AspNetCore.Components.RenderTree.NamedEventChange
Microsoft.AspNetCore.Components.RenderTree.NamedEventChange.AssignedName.get -> string!
Microsoft.AspNetCore.Components.RenderTree.NamedEventChange.ChangeType.get -> Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
Microsoft.AspNetCore.Components.RenderTree.NamedEventChange.ComponentId.get -> int
Microsoft.AspNetCore.Components.RenderTree.NamedEventChange.EventType.get -> string!
Microsoft.AspNetCore.Components.RenderTree.NamedEventChange.FrameIndex.get -> int
Microsoft.AspNetCore.Components.RenderTree.NamedEventChange.NamedEventChange() -> void
Microsoft.AspNetCore.Components.RenderTree.NamedEventChange.NamedEventChange(Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType changeType, int componentId, int frameIndex, string! eventType, string! assignedName) -> void
Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType.Added = 0 -> Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType.Removed = 1 -> Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
Microsoft.AspNetCore.Components.RenderTree.RenderBatch.NamedEventChanges.get -> Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.NamedEventChange>?
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentFrameFlags.get -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.ComponentRenderMode = 9 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.NamedEvent = 10 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ namespace Microsoft.AspNetCore.Components.RenderTree;
/// of the Blazor framework. These types will change in a future release.
/// </summary>
/// <remarks>
/// Constructs an instance of <see cref="NamedEvent"/>.
/// Constructs an instance of <see cref="NamedEventChange"/>.
/// </remarks>
/// <param name="changeType">The type of the change.</param>
/// <param name="componentId">The ID of the component holding the named value.</param>
/// <param name="frameIndex">The index of the <see cref="RenderTreeFrameType.NamedEvent"/> frame within the component's current render output.</param>
/// <param name="eventType">The event type.</param>
/// <param name="assignedName">The application-assigned name.</param>
public readonly struct NamedEvent(int componentId, int frameIndex, string eventType, string assignedName)
public readonly struct NamedEventChange(NamedEventChangeType changeType, int componentId, int frameIndex, string eventType, string assignedName)
{
/// <summary>
/// Describes the type of the change.
/// </summary>
public readonly NamedEventChangeType ChangeType { get; } = changeType;

/// <summary>
/// The ID of the component holding the named event.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.RenderTree;

/// <summary>
/// Describes a change to a named event.
/// </summary>
public enum NamedEventChangeType : int
{
/// <summary>
/// Indicates that the item was added.
/// </summary>
Added,

/// <summary>
/// Indicates that the item was removed.
/// </summary>
Removed,
}
15 changes: 4 additions & 11 deletions src/Components/Components/src/RenderTree/RenderBatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,22 @@ public readonly struct RenderBatch
public ArrayRange<ulong> DisposedEventHandlerIDs { get; }

/// <summary>
/// Gets the named events that were added, or null.
/// Gets the named events that were changed, or null.
/// </summary>
public ArrayRange<NamedEvent>? AddedNamedEvents { get; }

/// <summary>
/// Gets the named events that were removed, or null.
/// </summary>
public ArrayRange<NamedEvent>? RemovedNamedEvents { get; }
public ArrayRange<NamedEventChange>? NamedEventChanges { get; }

internal RenderBatch(
ArrayRange<RenderTreeDiff> updatedComponents,
ArrayRange<RenderTreeFrame> referenceFrames,
ArrayRange<int> disposedComponentIDs,
ArrayRange<ulong> disposedEventHandlerIDs,
ArrayRange<NamedEvent>? addedNamedEvents,
ArrayRange<NamedEvent>? removedNamedEvents)
ArrayRange<NamedEventChange>? changedNamedEvents)
{
UpdatedComponents = updatedComponents;
ReferenceFrames = referenceFrames;
DisposedComponentIDs = disposedComponentIDs;
DisposedEventHandlerIDs = disposedEventHandlerIDs;
AddedNamedEvents = addedNamedEvents;
RemovedNamedEvents = removedNamedEvents;
NamedEventChanges = changedNamedEvents;
}
}

20 changes: 8 additions & 12 deletions src/Components/Components/src/Rendering/RenderBatchBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ internal sealed class RenderBatchBuilder : IDisposable
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
public ArrayBuilder<int> DisposedComponentIds { get; } = new ArrayBuilder<int>();
public ArrayBuilder<ulong> DisposedEventHandlerIds { get; } = new ArrayBuilder<ulong>();
public ArrayBuilder<NamedEvent>? AddedNamedEvents;
public ArrayBuilder<NamedEvent>? RemovedNamedEvents;
public ArrayBuilder<NamedEventChange>? NamedEventChanges;

// Buffers referenced by UpdatedComponentDiffs
public ArrayBuilder<RenderTreeEdit> EditsBuffer { get; } = new ArrayBuilder<RenderTreeEdit>(64);
Expand Down Expand Up @@ -56,8 +55,7 @@ public void ClearStateForCurrentBatch()
DisposedComponentIds.Clear();
DisposedEventHandlerIds.Clear();
AttributeDiffSet.Clear();
AddedNamedEvents?.Clear();
RemovedNamedEvents?.Clear();
NamedEventChanges?.Clear();
}

public RenderBatch ToBatch()
Expand All @@ -66,8 +64,7 @@ public RenderBatch ToBatch()
ReferenceFramesBuffer.ToRange(),
DisposedComponentIds.ToRange(),
DisposedEventHandlerIds.ToRange(),
AddedNamedEvents?.ToRange(),
RemovedNamedEvents?.ToRange());
NamedEventChanges?.ToRange());

public void InvalidateParameterViews()
{
Expand All @@ -87,14 +84,14 @@ public void InvalidateParameterViews()

public void AddNamedEvent(int componentId, int frameIndex, ref RenderTreeFrame frame)
{
AddedNamedEvents ??= new();
AddedNamedEvents.Append(new NamedEvent(componentId, frameIndex, frame.NamedEventType, frame.NamedEventAssignedName));
NamedEventChanges ??= new();
NamedEventChanges.Append(new NamedEventChange(NamedEventChangeType.Added, componentId, frameIndex, frame.NamedEventType, frame.NamedEventAssignedName));
}

public void RemoveNamedEvent(int componentId, int frameIndex, ref RenderTreeFrame frame)
{
RemovedNamedEvents ??= new();
RemovedNamedEvents.Append(new NamedEvent(componentId, frameIndex, frame.NamedEventType, frame.NamedEventAssignedName));
NamedEventChanges ??= new();
NamedEventChanges.Append(new NamedEventChange(NamedEventChangeType.Removed, componentId, frameIndex, frame.NamedEventType, frame.NamedEventAssignedName));
}

public void Dispose()
Expand All @@ -104,7 +101,6 @@ public void Dispose()
UpdatedComponentDiffs.Dispose();
DisposedComponentIds.Dispose();
DisposedEventHandlerIds.Dispose();
AddedNamedEvents?.Dispose();
RemovedNamedEvents?.Dispose();
NamedEventChanges?.Dispose();
}
}
12 changes: 6 additions & 6 deletions src/Components/Components/src/Rendering/RenderTreeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public sealed class RenderTreeBuilder : IDisposable
private bool _hasSeenAddMultipleAttributes;
private Dictionary<string, int>? _seenAttributeNames;
private IComponentRenderMode? _pendingComponentCallSiteRenderMode; // TODO: Remove when Razor compiler supports call-site @rendermode
private (int Sequence, string AssignedName)? _pendingNamedSubmitEvent; // TODO: Remove when Razor compiler supports @onsubmit:name
private (int Sequence, string AssignedName)? _pendingNamedSubmitEvent; // TODO: Remove when Razor compiler supports @formname

/// <summary>
/// The reserved parameter name used for supplying child content.
Expand Down Expand Up @@ -80,7 +80,7 @@ public void CloseElement()
_entries.Buffer[indexOfEntryBeingClosed].ElementSubtreeLengthField = _entries.Count - indexOfEntryBeingClosed;
}

// TODO: Remove this once Razor supports @onsubmit:name
// TODO: Remove this once Razor supports @formname
private void CompletePendingNamedSubmitEvent()
{
if (_pendingNamedSubmitEvent is { } pendingNamedSubmitEvent)
Expand Down Expand Up @@ -237,9 +237,9 @@ public void AddAttribute(int sequence, string name, string? value)
AssertCanAddAttribute();
if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
// TODO: Remove this once the Razor compiler is updated to support @onsubmit:name
// That should compile directly as a call to AddNamedValue.
if (string.Equals(name, "@onsubmit:name", StringComparison.Ordinal) && _lastNonAttributeFrameType == RenderTreeFrameType.Element)
// TODO: Remove this once the Razor compiler is updated to support @formname
// That should compile directly as a call to AddNamedEvent.
if (string.Equals(name, "@formname", StringComparison.Ordinal) && _lastNonAttributeFrameType == RenderTreeFrameType.Element)
{
_pendingNamedSubmitEvent = (sequence, value!);
}
Expand Down Expand Up @@ -722,7 +722,7 @@ public void AddComponentRenderMode(int sequence, IComponentRenderMode renderMode
public void AddNamedEvent(int sequence, string eventType, string assignedName)
{
ArgumentNullException.ThrowIfNull(eventType);
ArgumentNullException.ThrowIfNull(assignedName);
ArgumentException.ThrowIfNullOrEmpty(assignedName);

// Note that we could trivially extend this to a generic concept of "named values" that exist within the rendertree
// and are tracked when added, removed, or updated. Currently we don't need that generality, but if we ever do, we
Expand Down
34 changes: 16 additions & 18 deletions src/Components/Components/test/RenderTreeDiffBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2237,10 +2237,9 @@ public void RecognizesNamedEventBeingAdded()
Assert.Equal(0, entry.ReferenceFrameIndex);
Assert.Equal("new element", referenceFrames[entry.ReferenceFrameIndex].ElementName);
});
Assert.Collection(batch.AddedNamedEvents.Value.AsEnumerable(),
entry => AssertNamedEvent(entry, 123, 2, "someevent1", "added to existing element"),
entry => AssertNamedEvent(entry, 123, 4, "someevent2", "added with new element"));
Assert.False(batch.RemovedNamedEvents.HasValue);
Assert.Collection(batch.NamedEventChanges.Value.AsEnumerable(),
entry => AssertNamedEventChange(entry, NamedEventChangeType.Added, 123, 2, "someevent1", "added to existing element"),
entry => AssertNamedEventChange(entry, NamedEventChangeType.Added, 123, 4, "someevent2", "added with new element"));
}

[Fact]
Expand All @@ -2264,10 +2263,9 @@ public void RecognizesNamedEventBeingRemoved()
// Assert
Assert.Collection(result.Edits,
entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 1));
Assert.False(batch.AddedNamedEvents.HasValue);
Assert.Collection(batch.RemovedNamedEvents.Value.AsEnumerable(),
entry => AssertNamedEvent(entry, 123, 2, "someevent1", "removing from retained element"),
entry => AssertNamedEvent(entry, 123, 4, "someevent2", "removed because element was removed"));
Assert.Collection(batch.NamedEventChanges.Value.AsEnumerable(),
entry => AssertNamedEventChange(entry, NamedEventChangeType.Removed, 123, 2, "someevent1", "removing from retained element"),
entry => AssertNamedEventChange(entry, NamedEventChangeType.Removed, 123, 4, "someevent2", "removed because element was removed"));
}

[Fact]
Expand All @@ -2293,10 +2291,9 @@ public void RecognizesNamedEventBeingMoved()
Assert.Equal(0, entry.ReferenceFrameIndex);
Assert.Equal("attr1", referenceFrames[entry.ReferenceFrameIndex].AttributeName);
});
Assert.Collection(batch.RemovedNamedEvents.Value.AsEnumerable(),
entry => AssertNamedEvent(entry, 123, 1, "eventname", "assigned name"));
Assert.Collection(batch.AddedNamedEvents.Value.AsEnumerable(),
entry => AssertNamedEvent(entry, 123, 2, "eventname", "assigned name"));
Assert.Collection(batch.NamedEventChanges.Value.AsEnumerable(),
entry => AssertNamedEventChange(entry, NamedEventChangeType.Removed, 123, 1, "eventname", "assigned name"),
entry => AssertNamedEventChange(entry, NamedEventChangeType.Added, 123, 2, "eventname", "assigned name"));
}

[Fact]
Expand All @@ -2317,10 +2314,9 @@ public void RecognizesNamedEventChangingAssignedName()

// Assert
Assert.Empty(result.Edits);
Assert.Collection(batch.RemovedNamedEvents.Value.AsEnumerable(),
entry => AssertNamedEvent(entry, 123, 1, "eventname1", "original name"));
Assert.Collection(batch.AddedNamedEvents.Value.AsEnumerable(),
entry => AssertNamedEvent(entry, 123, 1, "eventname1", "changed name"));
Assert.Collection(batch.NamedEventChanges.Value.AsEnumerable(),
entry => AssertNamedEventChange(entry, NamedEventChangeType.Removed, 123, 1, "eventname1", "original name"),
entry => AssertNamedEventChange(entry, NamedEventChangeType.Added, 123, 1, "eventname1", "changed name"));
}

private (RenderTreeDiff, RenderTreeFrame[]) GetSingleUpdatedComponent(bool initializeFromFrames = false)
Expand Down Expand Up @@ -2486,13 +2482,15 @@ private static void AssertPermutationListEntry(
Assert.Equal(toSiblingIndex, edit.MoveToSiblingIndex);
}

private static void AssertNamedEvent(
NamedEvent namedEvent,
private static void AssertNamedEventChange(
NamedEventChange namedEvent,
NamedEventChangeType type,
int componentId,
int frameIndex,
string eventType,
string assignedName)
{
Assert.Equal(type, namedEvent.ChangeType);
Assert.Equal(componentId, namedEvent.ComponentId);
Assert.Equal(frameIndex, namedEvent.FrameIndex);
Assert.Equal(eventType, namedEvent.EventType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2270,6 +2270,21 @@ public void CannotAddNamedEventWithNullAssignedName()
Assert.Equal("assignedName", ex.ParamName);
}

[Fact]
public void CannotAddNamedEventWithEmptyAssignedName()
{
// Arrange
var builder = new RenderTreeBuilder();
builder.OpenElement(0, "elem");

// Act/Assert
var ex = Assert.Throws<ArgumentException>(() =>
{
builder.AddNamedEvent(1, "eventtype", "");
});
Assert.Equal("assignedName", ex.ParamName);
}

[Fact]
public void CannotAddAttributesAfterNamedEvent()
{
Expand All @@ -2287,9 +2302,9 @@ public void CannotAddAttributesAfterNamedEvent()
}

[Fact]
public void TemporaryApiForNamedSubmitEventsWorksEvenIfAttributesAddedAfter()
public void TemporaryApiForFormNameEventsWorksEvenIfAttributesAddedAfter()
{
// TODO: Remove this once the Razor compiler is updated to support @onsubmit:name directly
// TODO: Remove this once the Razor compiler is updated to support @formname directly

// Arrange
var builder = new RenderTreeBuilder();
Expand All @@ -2298,7 +2313,7 @@ public void TemporaryApiForNamedSubmitEventsWorksEvenIfAttributesAddedAfter()
// Act
builder.OpenElement(0, "div");
builder.AddAttribute(1, "attr1", 123);
builder.AddAttribute(2, "@onsubmit:name", "some custom name");
builder.AddAttribute(2, "@formname", "some custom name");
builder.AddAttribute(3, "attr2", 456);
builder.OpenElement(4, "other");
builder.CloseElement();
Expand Down
Loading