第三章 组件(5)- 数据绑定
双向绑定功能
一、 @bind
在Blazor项目中,实现数据的双向绑定很简单,只需要在组件的HTML输入元素使用@bind
属性来绑定字段、属性或 Razor 表达式值即可。
- 实际上,
@bind
指令是@bind-value
指令的缩写形式,它绑定的是元素的value
属性。因此所有带有value
属性的HTML 元素都可以使用@bind
进行双向数据绑定 - 当进行双向绑定后,HTML元素对绑定数据的修改会触发组件的渲染
在下面实例中,在Bind组件中分别在两个Input
元素中通过@bind
属性指令进行字段和属性的绑定。当Input
元素失去焦点时,将更新其绑定的字段或属性。
-
示例-Bind.razor
@page "/bind" @rendermode InteractiveServer <PageTitle>Bind</PageTitle> <h1>Bind Example</h1> <p> <label> inputValue: <input @bind="inputValue" /> </label> </p> <p> <label> InputValue: <input @bind="InputValue" /> </label> </p> <ul> <li><code>inputValue</code>: @inputValue</li> <li><code>InputValue</code>: @InputValue</li> </ul> @code { private string? inputValue; private string? InputValue { get; set; } }
需要注意的是,在Blazor中,仅在渲染组件时才会更新UI来反应字段和属性的更新,而不会因为字段和属性的更新而立即做出响应。查看下面的例子,在组件初始化完成的三秒后,对InputValue属性进行了更改,但是可以发现更改后页面中第二个Input
框的值并没有发生变化,只有在第一个Input
框修改了内容并失焦后(引发渲染),才发生变化。
-
示例-Bind.razor
@page "/bind" @rendermode InteractiveServer <PageTitle>Bind</PageTitle> <h1>Bind Example</h1> <p> <label> inputValue: <input @bind="inputValue" /> </label> </p> <p> <label> InputValue: <input @bind="InputValue" /> </label> </p> <ul> <li><code>inputValue</code>: @inputValue</li> <li><code>InputValue</code>: @InputValue</li> </ul> @code { private string? inputValue; private string? InputValue { get; set; } protected override void OnInitialized() { Task.Run(() => { Thread.Sleep(3000); InputValue = "值被改变了"; }); } }
指定绑定事件
Input
元素中,使用@bind
时,默认的绑定事件是onchange
事件,也就是在输入框失焦时,才会对绑定数据进行更新。如果希望在其他的DOM事件上绑定数据,可以通过@bind:event="{EVENT}"
来指定({EVENT}
占位符表示对应的事件)。
下面的示例中,通过@bind:event="oninput"
将绑定时机修改到oninput
事件,这样当输入框发生变化时候就会更新绑定数据,而不需要等到失焦时才更新。
-
示例-BindEvent.razor
@page "/bind-event" @rendermode InteractiveServer <PageTitle>BindEvent</PageTitle> <h1>BindEvent Example</h1> <p> <label> InputValue: <input @bind="InputValue" @bind:event="oninput" /> </label> </p> <ul> <li><code>InputValue</code>: @InputValue</li> </ul> @code { public string? InputValue { get; set; } }
注意@bind:event
必须与@bind
同时存在,先有@bind
,再有@bind:event
。
指定回调方法
如果在绑定数据更新后,需要执行回调方法,可以使用@bind:after
来指定对应的方法。
-
需要注意的是,
@bind:after
支持异步回调,但是不支持EventCallback
和EventCallback<T>
。 -
示例-BindAfter.razor
@page "/bind-after" @rendermode InteractiveServer <PageTitle>BindAfter</PageTitle> <h1>BindAfter Example</h1> <input type="text" @bind="text" @bind:after="() => { }" /> <input type="text" @bind="text" @bind:after="After" /> <input type="text" @bind="text" @bind:after="AfterAsync" /> <h2>Components</h2> <InputText @bind-Value="text" @bind-Value:after="() => { }" /> <InputText @bind-Value="text" @bind-Value:after="After" /> <InputText @bind-Value="text" @bind-Value:after="AfterAsync" /> @code { private string text = ""; private void After() {} private Task AfterAsync() { return Task.CompletedTask; } }
二、@bind:get与@bind:set
上文中,使用了@bind
实现了数据的双向绑定,十分方便,但在实际的项目开发中很多时候会遇到需要将绑定数据的获取和设置分开进行。比如,希望在对绑定数据进行更新时,做一些逻辑处理。如果绑定的数据是一个属性,那还还好,可以通过set
来做一定的处理。但是如果绑定的是一个字段呢?或者说绑定的是个属性,但是逻辑较为复杂,不适合放在set
中的时候,就希望能将绑定数据的获取和设置分离开来。
Blazor为此提供了@bind:get
和@bind:set
,来解决这一问题,但在学习这对属性指令之前,先来看看通过事件触发能不能实现这个需求。
事件触发实现读写分离
仔细观察下面的例子,Input
元素使用@变量
和事件处理方法的方式来实现绑定数据的读写分离。
-
示例-ErrorBind.razor
@page "/error-bind" @rendermode InteractiveServer <p> <input value="@inputValue" @oninput="OnInput" /> </p> <p> <code>inputValue</code>: @inputValue </p> @code { private string? inputValue; private void OnInput(ChangeEventArgs args) { var newValue = args.Value?.ToString() ?? string.Empty; inputValue = newValue.Length > 4 ? "Long!" : newValue; } }
通过直接事件触发,来实现数据双向绑定的做法是有弊端的!
在上面的例子中,在提供第四个字符后,OnInput
事件处理程序将 inputValue
的值更新为 Long!
。 但是,用户可以继续在 UI 中向元素值添加字符。 inputValue
的值并没有随着每次击键而绑定回元素的值,只能进行单向数据绑定。原因是Blazor 不知道代码打算在事件处理程序中修改 inputValue
的值。 Blazor 不会尝试强制 DOM 元素值和 .NET 变量值进行匹配,除非它们通过 @bind
语法绑定。
- 注意,实际上即使例子中换成了
@bind
和事件触发的方式也是存在问题的,如果直接绑定属性,并且在set
中做处理就不会。
@bind:get
与@bind:set
为了应对绑定数据读写分离上的问题,Blazor提供了一对属性指令,即@bind:get
与@bind:set
。
-
@bind:get
:指定要绑定的字段、属性。 -
@bind:set
:指定给字段/属性设置值的回调,绑定的是 C#方法,该方法符合Func<T,Task>
或Action<T>
委托的声明要求。默认情况下会使用onchange
事件调用@bind:set
绑定的方法,也就是输入框失去焦点后执行。 -
@bind:get
和@bind:set
修饰符始终一起使用。 -
示例-RightBind.razor
@page "/right-bind" @rendermode InteractiveServer <p> <input @bind:get="inputValue" @bind:set="OnInputAsync" @bind:event="oninput" /> </p> <p> <code>inputValue</code>: @inputValue </p> @code { private string? inputValue; private Task OnInputAsync(string? value) { var newValue = value ?? string.Empty; inputValue = newValue.Length > 4 ? "Long!" : newValue; return Task.CompletedTask; } }
一般情况下,如果逻辑较为简单,那么通过@bind
直接绑定到属性,在属性的set
上去做处理同样可以解决问题。
三、使用 <select>
元素的多个选项选择
@bind
支持使用 <select>
元素的 multiple
多项选择。
下面例子中,分别使用事件和@bind
实现了类似的效果,可以进行对比参考。
-
BindMultipleInput.razor
@page "/bind-multiple-input" @rendermode InteractiveServer <h1>Bind Multiple <code>input</code>Example</h1> <p> <label> Select one or more cars: <select @onchange="SelectedCarsChanged" multiple> <option value="audi">Audi</option> <option value="jeep">Jeep</option> <option value="opel">Opel</option> <option value="saab">Saab</option> <option value="volvo">Volvo</option> </select> </label> </p> <p> Selected Cars: @string.Join(", ", SelectedCars) </p> <p> <label> Select one or more cities: <select @bind="SelectedCities" multiple> <option value="bal">Baltimore</option> <option value="la">Los Angeles</option> <option value="pdx">Portland</option> <option value="sf">San Francisco</option> <option value="sea">Seattle</option> </select> </label> </p> <span> Selected Cities: @string.Join(", ", SelectedCities) </span> @code { public string[] SelectedCars { get; set; } = new string[] { }; public string[] SelectedCities { get; set; } = new[] { "bal", "sea" }; private void SelectedCarsChanged(ChangeEventArgs e) { if (e.Value is not null) { SelectedCars = (string[])e.Value; } } }
将 <select>
元素选项绑定到 C# 对象 null
值
由于以下原因,没有将 <select>
元素选项值表示为 C# 对象 null
值的合理方法:
- HTML 属性不能具有
null
值。 HTML 中最接近的null
等效项是<option>
元素中缺少 HTMLvalue
属性。 - 选择没有
value
属性的<option>
时,浏览器会将该<option>
的元素的文本内容视为value
值。
HTML 中最合理的 null
等效项是空字符串value
。 Blazor 框架处理 null
到空字符串之间的转换,以便双向绑定到 <select>
的值。
数据处理
字符串格式化
可以使用@bind:format="{FORMAT STRING}"
对绑定数据进行格式化。
-
目前Blazor中暂时只支持对日期的字符串格式化,对于其他格式表达式(如货币或数字格式)暂时不可用。
-
DataBinding.razor
@page "/date-binding" <PageTitle>Date Binding</PageTitle> <h1>Date Binding Example</h1> <p> <label> <code>yyyy-MM-dd</code> format: <input @bind="startDate" @bind:format="yyyy-MM-dd" /> </label> </p> <p> <code>startDate</code>: @startDate </p> @code { private DateTime startDate = new(2020, 1, 1); }
无法分析的值
如果用户向数据绑定元素提供无法分析的值,则在触发绑定事件时,无法分析的值会自动还原为以前的值。例如绑定的数据类型为int
,但是输入了100.22,如果本来的值为100,则会在失焦(onchange
)时还原为100或在更改时(oninput
)保留100。
处理方案
无论是格式化还是遇到无法分析的值,实际上都可以通过其他方式解决,比如使用 @bind:get
/@bind:set
修饰符(如本文前面所述)或使用自定义 get
和 set
访问器逻辑绑定到属性来处理无效输入。或者将输入组件(例如 InputNumber<TValue>
或 InputDate<TValue>
)与窗体验证配合使用。
组件参数的双向绑定
一、单向绑定
默认情况下,组件参数的传递是单向的,这个单向的意思是,当父组件中使用了某个子组件,并给子组件设置了某个参数值,会出现如下情况:
- 如果父组件发生了渲染并且给子组件设置的参数值发生了变化,值就会传递到子组件并引发子组件的渲染。
- 如果是子组件对自身的组件参数发生的修改,即使子组件渲染后自身更新了对应的数据,但对父组件而言,是没有任何影响的。
如下面示例中,点击子组件的按钮,只能修改自身的展示数据。
-
子组件-ParamTestChild.razor
<div> <label>子控件:</label> <input type="text" value="@Data" /> <button @onclick="SetParam">按钮</button> </div> @code { [Parameter] public int Data { get; set; } private void SetParam(MouseEventArgs e) { Data = 300; } }
-
父组件-ParamTestFather.razor
@page "/param-test" @rendermode InteractiveServer @using BlazorAppServer.Components.Common <h3>ParamTestFather</h3> <ParamTestChild Data="@fatherData" /> <label>父控件:</label> <input type="text" value="@fatherData" /> <button @onclick="SetParam">按钮</button> @code { private int fatherData { get; set; } private void SetParam(MouseEventArgs e) { fatherData = 100; } }
二、双向绑定
想要对组件参数进行双向数据绑定,常见方案是将子组件中的属性绑定到其父组件中的属性。 此方案称为链接绑定,因为多个级别的绑定会同时进行。
具体做法如下
在子组件中定义组件参数,提供给父组件进行数据绑定
[Parameter]
public int Year { get; set; }
在子组件中定义EventCallback
/EventCallback<T>
事件处理方法,以支持从子组件更新父组件中的属性。
- 其组件参数名称,建议按照约定进行命名,其命名约定为
{PARAMETERNAME}Changed
。
[Parameter]
public EventCallback<int> YearChanged { get; set; }
private async Task UpdateYearFromChild()
{
await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021));
}
在父组件中使用@bind-{PropertyName}
对组件参数进行数据绑定,@bind-event:{EventCallBackName}
指定对应的事件处理程序。
<ChildBind @bind-Year="year" @bind-Year:event="YearChanged" />
@* 如果事件处理方法的命名按照约定来,那么可以简写如下 *@
<ChildBind @bind-Year="year"/>
完整示例
-
ChildBind.razor
<div class="card bg-light mt-3" style="width:18rem "> <div class="card-body"> <h3 class="card-title">ChildBind Component</h3> <p class="card-text"> Child <code>Year</code>: @Year </p> <button @onclick="UpdateYearFromChild">Update Year from Child</button> </div> </div> @code { [Parameter] public int Year { get; set; } [Parameter] public EventCallback<int> YearChanged { get; set; } private async Task UpdateYearFromChild() { await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021)); } }
-
Parent.razor
@page "/parent" @rendermode InteractiveServer <PageTitle>Parent</PageTitle> <h1>Parent Example</h1> <p>Parent <code>year</code>: @year</p> <button @onclick="UpdateYear">Update Parent <code>year</code></button> <ChildBind @bind-Year="year"/> @code { private int year = 1979; private void UpdateYear() { year = Random.Shared.Next(1950, 2021); } }
链接绑定多个组件的组件参数
链接绑定多个组件的组件参数,其实就是组件参数双向绑定的嵌套用法。
可以将参数绑定到任意数量的嵌套组件,但必须采用单向数据流:
- 更改通知沿层次结构向上传递。
- 新参数值按层次结构向下传递。
需要注意的是,在实现过程中,定义的EventCallback
/EventCallback<T>
事件处理方法,其名字必须根据约定来,否则会报错。
仔细查看下面的例子,其中最需要注意的是NestedChild.razor组件:
-
使用
@bind:get
语法将ChildMessage的值分配给GrandchildMessage -
使用
@bind:set
绑定ChildMessageChanged事件处理程序,当NestedGrandchild组件中,对GrandchildMessage发生变化时,执行ChildMessageChanged事件处理程序,从而完成对父组件的绑定数据的更新。 -
NestedGrandchild.razor
<div class="border rounded m-1 p-1"> <h3>Grandchild Component</h3> <p>Grandchild Message: <b>@GrandchildMessage</b></p> <p> <button @onclick="ChangeValue">Change from Grandchild</button> </p> </div> @code { [Parameter] public string? GrandchildMessage { get; set; } [Parameter] public EventCallback<string> GrandchildMessageChanged { get; set; } private async Task ChangeValue() { await GrandchildMessageChanged.InvokeAsync( $"Set in Grandchild {DateTime.Now}"); } }
-
NestedChild.razor
<div class="border rounded m-1 p-1"> <h2>Child Component</h2> <p>Child Message: <b>@ChildMessage</b></p> <p> <button @onclick="ChangeValue">Change from Child</button> </p> <NestedGrandchild @bind-GrandchildMessage:get="ChildMessage" @bind-GrandchildMessage:set="ChildMessageChanged" /> </div> @code { [Parameter] public string? ChildMessage { get; set; } [Parameter] public EventCallback<string?> ChildMessageChanged { get; set; } private async Task ChangeValue() { await ChildMessageChanged.InvokeAsync( $"Set in Child {DateTime.Now}"); } }
-
Parent.razor
@page "/parent" @rendermode InteractiveServer <PageTitle>Parent 2</PageTitle> <h1>Parent Example 2</h1> <p>Parent Message: <b>@parentMessage</b></p> <p> <button @onclick="ChangeValue">Change from Parent</button> </p> <NestedChild @bind-ChildMessage="parentMessage"/> @code { private string parentMessage = "Initial value set in Parent"; private void ChangeValue() { parentMessage = $"Set in Parent {DateTime.Now}"; } }