ASP.NET Core Blazor Webassembly 之 数据绑定
上一次我們學習了Blazor組件相關的知識(Asp.net Core Blazor Webassembly - 組件)。這次繼續學習Blazor的數據綁定相關的知識。當代前端框架都離不開數據綁定技術。數據綁定技術以數據為主導來驅動UI界面,用戶對數據的修改會實時提現在UI上,極大的提高了開發效率,讓開發者從繁瑣的dom操作中解脫出來。對于數據綁定.NET開發者并不會陌生,WPF里大量應用數據綁定技術,有過WPF開發經驗的同學其實很容易理解前端的數據綁定。總之數據綁定技術及其概念、思維極其重要。下面讓我們看看Blazor的數據綁定技術。
單向綁定
Blazor的數據綁定官方文檔是直接從雙向綁定開始的,但我覺得有必要說一下單向綁定。因為其他框架一般都會區分單向、雙向,比如vue的v-bind單向,v-model就是雙向。我們這里分開講也有利于跟其他框架進行對比。下面我們實現一個計數器組件來演示下單向數據綁定。
使用@進行綁定
@page "/counter" <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code {private int currentCount = 0;private void IncrementCount(){currentCount++;} }這個Counter組件默認的項目就自帶。跟我們使用服務端Razor一樣,使用@符號在需要替換值的地方插入對應的變量。這個值就會被渲染在相應的地方。當我們在前端修改變量的時候,對應的ui界面會同步進行修改。
使用@bind-{attribute}進行綁定
除了直接使用@進行綁定,我們還可以使用@bind-{attribute}來實現對html元素屬性的綁定,比如對style,class內容進行綁定。下面演示下對class進行綁定。我們把p元素的class綁定到“currentClass”字段。
@page "/counter" <h1>Counter</h1> <p @bind-class="currentClass" @bind-class:event="onchange">current count: @currentCount </p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code {private string currentClass = "text-danger";private int currentCount = 0;private void IncrementCount(){currentCount++;} }使用@bind-{attribute}進行綁定有個比較奇怪的問題,當你使用@bind-{attribute}進行綁定的時候必須同時指定@bind-{attribute}:event。@bind-{attribute}:event是用來指定雙向綁定的時候控件在發生某個事件的時候回寫值到綁定的字段上。可是p,div這種元素根本不可能會激發onchange,oninput這種事件,也不可能去修改綁定的字段的值,這個用法感覺有點多此一舉。Blazor的單向數據綁定的用法跟ASP.NET Core MVC的Razor基本相似,不同點就是Blazor不需要Http回發到服務器就可以實時渲染新的界面出來。
雙向綁定
雙向綁定主要使用在一些輸入控件上,比如input,select等。當我們對這些控件上的值進行修改后會回寫綁定的字段。這種特性在表單場景中非常有用。我們定義一個用戶信息編輯的組件來演示下:
@page "/infoedit" <p>userName: @userName </p> <p>sex: @sex </p> <p>userName: <input @bind="userName" /> </p> <p>sex:<select @bind="sex"><option value="m">男</option><option value="f">女</option></select> </p> @code {private string userName="abc";private string sex="f"; }當我們運行這個組件,在文本框進行修改后,鼠標點擊其他地方讓文本框失去焦點值就會回寫到綁定的字段上,上面的單向綁定信息會自動同步。但是如果你用過VUE或者Angularjs的雙向綁定就會覺得失去焦點再回寫字段數據太慢了,一點也不酷。要知道VUE的雙向綁定可是實時同步的,那么Blazor如何做到在輸入的同時就更新值呢,答案是使用@bind:event來指定回寫的激發事件,我們改成“oninput”事件就可以實現:
<p>userName: <input @bind="userName" @bind:event="oninput"/> </p>雙向綁定的多種寫法
看到這里也許你也明白了,@bind真正的本質是由對value的綁定和對某個事件的綁定協同完成的。這點跟VUE非常相似。@bind其實是@bind-value的縮寫,我們可以用@bind-value來實現雙向綁定:
<p>userName: <input @bind-value="userName" @bind-value:event="oninput"/> </p>以上寫法的效果跟@bind一模一樣。再進一步,@bind-value也只是對@的包裝,我們可以使用@來實現雙向綁定:
@page "/infoedit" <p>userName: @userName </p> <p>sex: @sex </p> <p>userName: <input value="@userName" @oninput="oninput"/> </p> <p>sex:<select @bind="sex"><option value="m">男</option><option value="f">女</option></select> </p> @code {private string userName="abc";private string sex="f";private void oninput(ChangeEventArgs e){userName = e.Value.ToString();} }以上代碼的效果跟@bind一模一樣。通過使用@對value直接進行綁定以及綁定一個oninput事件進行值的回寫,同樣實現了雙向綁定。
格式化時間字符串
使用@bind:format 可以對綁定時間類型字段的時候進行格式化:
出生日期:<input @bind="birthDay" @bind:format="yyyy-MM-dd" />這個功能有點類似Angularjs的filter功能,但是目前只能對時間進行格式化,功能很弱。
父組件綁定數據到子組件
組件之間往往都是嵌套的,很多子組件都依賴父組件的數據來決定如何呈現,這種場景非常常見。我們還是繼續修改上面的編輯組件,用戶信息不在自己初始化,而是從父組件傳遞過來:
子組件:
子組件定義一個UserInfo對象并且使用[Parameter]進行標記,同時如果父組件使用@bind-UserInfo來綁定的話,還必須實現一個UserInfoChanged事件。
父組件:
父組件初始化一個UserInfo對象后通過@bind-UserInfo綁定給子組件。
注意這里我們修改子組件的值并不會同步給父組件,所以可以看到@bind-UserInfo的傳值還是單向的。
子組件傳值給父組件 ??
原來我以為父組件使用@bind-UserInfo并且子組件實現了對應的changed方法就可以實現子組件跟父組件的自動傳值,就跟input的雙向綁定一樣。但是不管我怎么試都沒有卵用。如果只是單向的那為什么要這么大費周章?我直接使用屬性賦值不就可以了么?像下面這樣:
<InfoEdit UserInfo="userInfo" ></InfoEdit>直接通過組件的屬性直接把父組件的數據傳遞到子組件,效果跟上面是一樣的,而且這樣子組件我還能少寫一個changed事件。我原本以為使用基本類型,比如string可以自動雙向綁定,然后并沒有什么卵用。沒有辦法我繼續嘗試父組件監聽UserInfoChanged事件來接受子組件的數據,然后VS提示我同一個事件不能綁定兩次。
我已經無語了,難道要我再定義一個事件嗎?于是我放棄了@bind-來實現子組件給父組件傳值,我直接使用屬性賦值難道不比這個簡單嗎?子組件修改數據的時候不斷對外拋事件:
====================child================== <p>userName: <input @bind="UserInfo.UserName" @oninput="InvokeChanged"/> </p> <p>sex:<select @bind="UserInfo.Sex"><option value="m">男</option><option value="f">女</option></select> </p> <p>BrithDay:<input @bind="UserInfo.BrithDay" /> </p> @code {[Parameter]public UserInfo UserInfo { get; set; }[Parameter]public EventCallback<UserInfo> UserInfoChanged { get; set; }private void InvokeChanged(){UserInfoChanged.InvokeAsync(this.UserInfo);Console.WriteLine("InvokeChanged");} }父組件監聽事件后更新數據:
@page "/" ====================parent```================== <p>userName: @userInfo.UserName </p> <p>sex: @userInfo.Sex </p> <p>brithday: @userInfo.BrithDay </p> <p>title: @title </p> <InfoEdit UserInfo="userInfo" UserInfoChanged="HandleUserInfoChanged"></InfoEdit> @code {private UserInfo userInfo;private string title;protected override void OnInitialized(){userInfo = new UserInfo{UserName = "abc",Sex = "f",BrithDay = DateTime.Now};base.OnInitialized();}private void HandleUserInfoChanged(UserInfo info){this.userInfo.UserName = info.UserName;Console.WriteLine("HandleUserInfoChanged");} }我原以為這樣就沒什么問題了,可奇怪的是,父組件頁面重新渲染需要在子組件第二次修改數據后呈現且呈現的是前一次的。
到這里我已經無語了,最后我只能在子組件直接添加一個按鈕,修改完后點擊保存來觸發InvokeChanged事件,這樣子是可以的:
====================child================== <p>userName: <input @bind="UserInfo.UserName" /> </p> <p>sex:<select @bind="UserInfo.Sex"><option value="m">男</option><option value="f">女</option></select> </p> <p>BrithDay:<input @bind="UserInfo.BrithDay" /> </p> <button class="btn btn-danger" @onclick="InvokeChanged">保存</button> @code {[Parameter]public UserInfo UserInfo { get; set; }[Parameter]public EventCallback<UserInfo> UserInfoChanged { get; set; }private void InvokeChanged(){UserInfoChanged.InvokeAsync(this.UserInfo);Console.WriteLine("InvokeChanged");} }到此數據綁定也演示完了,可是關于子組件往父組件傳值的事我實在沒像明白,難道是我哪里錯了?
終于搞定子組件往父組件傳值
接上面,當子組件綁定父組件的一個字段,并且子組件修改它的時候父組件不能實時進行同步更新UI的問題,最近終于在Blazui作者的指導下搞定了。
UserInfo類要實現INotifyPropertyChanged接口
public class UserInfo: INotifyPropertyChanged{private string _userName;public string UserName {get{return _userName;}set{_userName = value;PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UserName)));}}public string Sex { get; set; }public DateTime BrithDay { get; set; }public event PropertyChangedEventHandler PropertyChanged;}沒想到微軟blazor還是借用了WPF搞MVVM的模式,模型需要實現INotifyPropertyChanged類,在屬性發生修改的時候可以發出通知。
父組件訂閱PropertyChanged事件:
@page "/" ====================parent```================== <p>userName: @userInfo.UserName </p> <p>sex: @userInfo.Sex </p> <p>brithday: @userInfo.BrithDay </p> <p>title: @title </p> <InfoEdit UserInfo="userInfo" UserInfoChanged="HandleUserInfoChanged"></InfoEdit> @code {private UserInfo userInfo;private string title;protected override void OnInitialized(){userInfo = new UserInfo{UserName = "abc",Sex = "f",BrithDay = DateTime.Now};this.userInfo.PropertyChanged += (o, e) => StateHasChanged();base.OnInitialized();}private void HandleUserInfoChanged(UserInfo info){this.userInfo = info;Console.WriteLine("HandleUserInfoChanged");} }父組件訂閱子組件的PropertyChanged事件,當事件發生的時候調用組件的StateHasChanged方法。StateHasChanged方法會通知組件說狀態發生變化了,也就是說組件會被重新渲染。這就是最關鍵的東西了。
子組件
====================child================== <p>userName: <input @bind="UserInfo.UserName" @bind:event="oninput" /> </p> <p>sex:<select @bind="UserInfo.Sex"><option value="m">男</option><option value="f">女</option></select> </p> <p>BrithDay:<input @bind="UserInfo.BrithDay" /> </p> <button class="btn btn-danger" @onclick="InvokeChanged">保存</button> @code {[Parameter]public UserInfo UserInfo { get; set; }[Parameter]public EventCallback<UserInfo> UserInfoChanged { get; set; }private void InvokeChanged(){UserInfoChanged.InvokeAsync(this.UserInfo);Console.WriteLine("InvokeChanged");} }運行
一些吐槽
雖然搞定了子父組件同步的問題,但是我不能理解的是,為什么微軟要搞的這么復雜。使用@bind-UserInfo會強制用戶在子組件實現一個 EventCallbackUserInfoChanged 事件。那么既然@bind:event="oninput"可以實時回寫字段的值,那么為什么不直接同時調用UserInfoChanged對外拋事件呢?而且在父組件同樣可以在編譯器直接植入對UserInfoChanged事件的監聽同時刷新UI。可能是微軟為了性能,想要用戶手工控制父組件的渲染時機吧。
相關內容:
ASP.NET Core Blazor 初探之 Blazor Server
ASP.NET Core Blazor 初探之 Blazor WebAssembly
關注我的公眾號一起玩轉技術
總結
以上是生活随笔為你收集整理的ASP.NET Core Blazor Webassembly 之 数据绑定的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中移动完成透镜天线远距覆盖和降本增效试点
- 下一篇: ASP.NET Core 注册单例方案