Skip to content

Commit e9956c3

Browse files
committed
Add support for binding to MarkupString values
It's quite convenient to be able to bind directly to data containing raw HTML. This is quite convoluted to do today, requiring registering a TypeDescriptionProvider, which in turn returns a custom type descriptor which in turn can provide the missing TypeConverter for the desired type. This adds support out of the box for binding straight to markup strings. Follows the implementation and tests from dotnet#10730. Closes dotnet#47718
1 parent fdd5c92 commit e9956c3

File tree

4 files changed

+77
-0
lines changed

4 files changed

+77
-0
lines changed

src/Components/Components/src/MarkupString.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.ComponentModel;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Globalization;
7+
48
namespace Microsoft.AspNetCore.Components;
59

610
/// <summary>
711
/// A string value that can be rendered as markup such as HTML.
812
/// </summary>
13+
[TypeConverter(typeof(MarkupStringTypeConverter))]
914
public readonly struct MarkupString
1015
{
1116
/// <summary>
@@ -32,4 +37,33 @@ public static explicit operator MarkupString(string value)
3237
/// <inheritdoc />
3338
public override string ToString()
3439
=> Value ?? string.Empty;
40+
41+
private class MarkupStringTypeConverter : TypeConverter
42+
{
43+
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
44+
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
45+
46+
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
47+
{
48+
if (value is string markup)
49+
{
50+
return (MarkupString)markup;
51+
}
52+
53+
return base.ConvertFrom(context, culture, value);
54+
}
55+
56+
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
57+
=> destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
58+
59+
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
60+
{
61+
if (destinationType == typeof(string) && value is MarkupString markup)
62+
{
63+
return markup.Value ?? "";
64+
}
65+
66+
return base.ConvertTo(context, culture, value, destinationType);
67+
}
68+
}
3569
}

src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,25 @@ public async Task CreateBinder_Guid()
556556
Assert.Equal(1, component.Count);
557557
}
558558

559+
[Fact]
560+
public async Task CreateBinder_MarkupString()
561+
{
562+
// Arrange
563+
var value = new MarkupString();
564+
var component = new EventCountingComponent();
565+
Action<MarkupString> setter = (_) => value = _;
566+
567+
var binder = EventCallback.Factory.CreateBinder(component, setter, value);
568+
569+
var expectedValue = new MarkupString("<br/>");
570+
571+
// Act
572+
await binder.InvokeAsync(new ChangeEventArgs() { Value = expectedValue.ToString(), });
573+
574+
Assert.Equal(expectedValue, value);
575+
Assert.Equal(1, component.Count);
576+
}
577+
559578
// This uses a type converter
560579
[Fact]
561580
public async Task CreateBinder_NullableGuid()

src/Components/test/E2ETest/Tests/BindTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,23 @@ public void CanBindTextboxGenericGuid()
716716
Assert.Equal(newValue, mirrorValue.GetAttribute("value"));
717717
}
718718

719+
[Fact]
720+
public void CanBindTextboxMarkupString()
721+
{
722+
var target = Browser.Exists(By.Id("textbox-markup"));
723+
var boundValue = Browser.Exists(By.Id("textbox-markup-value"));
724+
var mirrorValue = Browser.Exists(By.Id("textbox-markup-mirror"));
725+
Assert.Equal("", target.GetAttribute("value"));
726+
Assert.Equal("", boundValue.Text);
727+
Assert.Equal("", mirrorValue.GetAttribute("value"));
728+
729+
// Modify target; verify value is updated and that textboxes linked to the same data are updated
730+
var newValue = "hello";
731+
target.SendKeys(newValue + "\t");
732+
Browser.Equal(newValue, () => boundValue.Text);
733+
Assert.Equal(newValue, mirrorValue.GetAttribute("value"));
734+
}
735+
719736
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
720737
// Blazor have different formatting behaviour by default.
721738
[Fact]

src/Components/test/testassets/BasicTestApp/BindCasesComponent.razor

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@
123123
<span id="textbox-generic-guid-value">@textboxGenericGuidValue</span>
124124
<input id="textbox-generic-guid-mirror" @bind="textboxGenericGuidValue" readonly />
125125
</p>
126+
<p>
127+
Generic bind (markup):
128+
<BindGenericComponent Id="textbox-markup" @bind-Value="textboxGenericMarkupValue" />
129+
<span id="textbox-markup-value">@textboxGenericMarkupValue</span>
130+
<input id="textbox-markup-mirror" @bind="textboxGenericMarkupValue" readonly />
131+
</p>
126132

127133
<h2>Date Textboxes (type=text)</h2>
128134
<p>
@@ -500,6 +506,7 @@
500506

501507
int textboxGenericIntValue = -42;
502508
Guid textboxGenericGuidValue = Guid.Empty;
509+
MarkupString textboxGenericMarkupValue = new MarkupString();
503510

504511
DateTime textboxDateTimeValue = new DateTime(1985, 3, 4);
505512
DateTime? textboxNullableDateTimeValue = null;

0 commit comments

Comments
 (0)