1.環境
VS2019 16.5.1
.NET Core SDK 3.1.200
Blazor WebAssembly Templates 3.2.0-preview2.20160.5
2.簡介
在使用Blazor時,避免不了要進行組件間通信,組件間的通信大致上有以下幾種:
(1) 父、子組件間通信;
(2) 多級組件組件通信,例如祖、孫節點間通信;
(3) 非嵌套組件間通信。
Blazor支持數據的雙向綁定,這里主要介紹單向綁定的實現。
3.父、子組件間通信
父、子組件間通信分為兩類:父與子通信、子與父通信。
3.1.父與子通信
父與子通信是最為簡單的,直接通過數據綁定即可實現:
Self1.razor
<div class="bg-white p-3" style="color: #000;"> <h3>Self1</h3> <p>parent: @Value</p> </div> @code { [Parameter] public string Value { get; set; } }
Parent1.razor
<div class="bg-primary jumbotron text-white"> <h3>Parent1</h3> <Self1 Value="I'm from Parent1"></Self1> </div>
效果如下:
3.2.子與父通信
子與父通信是通過回調事件實現的,通過將事件函數傳遞到子組件,子組件在數據發生變化時,調用該回調函數即可。在Self1.razor和Parent1.razor組件上進行修改,為Self1組件基礎上添加一個事件OnValueChanged,並在數據Value發生變化時執行該事件,通知父組件新數據是什么,在這里,我沒有在子組件中更新Value的值,因為新的數據會從父組件流到子組件中。現在的得到的組件Self2.razor和Parent2.razor的代碼如下:
Self2.razor
<div class="bg-white p-3" style="color: #000;"> <h3>Self1</h3> <button @onclick="ChangeValue">ChangeValue</button> <p>parent: @Value</p> </div> @code { [Parameter] public string Value { get; set; } [Parameter] public EventCallback<string> OnValueChanged { get; set; } private async Task ChangeValue() { string newValue = DateTime.Now.ToString("o"); if (OnValueChanged.HasDelegate) { await OnValueChanged.InvokeAsync(newValue); } } }
Parent2.razor
<div class="bg-primary jumbotron text-white"> <h3>Parent2</h3> <p>@_value</p> <Self2 Value="@_value" OnValueChanged="@OnValueChanged"></Self2> </div> @code{ private string _value = "I'm from Parent2"; private void OnValueChanged(string val) { _value = val; } }
效果如下:
3.3.使用@bind
@bind支持數據的雙向綁定,但是當子組件發生變化時,依然需要調用回調事件,不過好處就是回調事件不用你寫,這個在blazor入門筆記(5)-數據綁定中有實現。
4.祖、孫組件間通信
祖、孫組件間的通信也分為兩類:祖與孫通信、孫與祖通信。最暴力的方法就是通過父節點中轉,實現祖-父-孫通信,但是當跨越多個層級的時候就比較麻煩,好在Blazor提供了“Cascading values and parameters”,中文翻譯為級聯值和參數。級聯值和參數是通過CascadingValue組件和CascadingParameter屬性注解實現的。
4.1.祖與孫通信
先上代碼:
Self3.razor
<div class="bg-white p-3" style="color: #000;"> <h3>Self3</h3> <p>GrandValue: @GrandValue</p> </div> @code { /// <summary> /// Name參數必須與Name帶有CascadingValue組件的屬性匹配,如果我們沒有注明Name,則會通過類型匹配一個最相似的屬性 /// </summary> [CascadingParameter(Name = "GrandValue")] string GrandValue { get; set; } }
Parent3.razor
<div class="bg-primary jumbotron text-white"> <h3>Parent3</h3> <Self3></Self3> </div>
Grand3.Razor
<h3>Grand3</h3> <p>GrandValue:@_grandValue</p> <CascadingValue Value="@_grandValue" Name="GrandValue"> <Parent3 /> </CascadingValue> @code { private string _grandValue = "GrandValue"; }
我們在Grand3組件中使用CascadingValue組件包裹了Parent3組件,並為組件添加了一個Value參數和一個Name參數,並將_grandValue賦給了Value。Parent3組件中沒有做任何事情,僅使用Self3組件。在Self3中聲明了一個GrandValue的屬性,並在這個屬性上使用了CascadingParameter屬性注解,CascadingParameter指定了Name為在Grand3組件中CascadingValue組件的Name參數的值。這樣,我們就可以在Self3組件中獲取到Grand3組件中的_grandValue值。效果如下:
注意:
(1) CascadingParameter所聲明的屬性可以是private
(2) CascadingValue和CascadingParameter可以不指定Name,這時將會通過類型進行匹配。
當我們如果有多個參數需要從祖傳遞到孫怎么辦呢?有兩種方法:
(1) 嵌套使用CascadingValue
CascadingValue組件運行嵌套使用,可以在祖組件中嵌套CascadingValue,而孫組件中則只需要將所有的來自祖組件的參數使用CascadingParameter進行聲明即可。需要注意的是,如果指定Name,請確保每個Name都是唯一的。
(2) 使用Model類
CascadingValue可以是class,因此可以將所有的需要傳遞的參數使用一個class進行封裝,然后傳遞到孫組件,孫組件使用同類型的class接收該參數即可。
4.2.孫與祖通信
孫與祖通信與子與父通信一樣,需要使用事件進行回調,這個回調方法也是一個參數,因此只需要將該回調也通過CascadingValue傳遞到孫組件中,當孫組件數據發生變化時調用該回調函數即可。傳遞的方法如4.1.所示有兩種,但是無論哪種都需要在祖組件中手動調用StateHasChanged。另外,如果直接更新值或者引用,請不要在孫組件中直接更新,只需要調用回掉即可,因為會觸發兩次渲染(可以在代碼的GrandX看到)。當然,如果是引用中的值,比如model中的值,是需要在子組件中更新的。 這里我們將參數和回調封裝成一個類:
public class CascadingModel<T> { public CascadingModel() { } public CascadingModel(T defaultValue) { _value = defaultValue; } public Action StateHasChanged; private T _value; public T Value { get => _value; set { _value = value; StateHasChanged?.Invoke(); } } }
組件中代碼如下:
Self4.razor
<div class="bg-white p-3" style="color: #000;"> <h3>Self4</h3> <p>GrandValueModel-GrandValue: @CascadingModel.Value</p> <button @onclick="ChangGrandValue">Chang GrandValue</button> </div> @code { [CascadingParameter(Name = "GrandValue")] CascadingModel<string> CascadingModel { get; set; } void ChangGrandValue() { CascadingModel.Value = "I'm Form self:" + DateTime.Now.ToString("HH:mm:ss"); } }
Parent4.razor
<div class="bg-primary jumbotron text-white"> <h3>Parent4</h3> <Self4></Self4> </div>
Grand4.razor
<h3>Grand4</h3> <p>GrandValue:@_cascadingModel.Value</p> <CascadingValue Value="@_cascadingModel" Name="GrandValue"> <Parent4 /> </CascadingValue> @code { private CascadingModel<string> _cascadingModel = new CascadingModel<string>("GrandValue"); protected override void OnInitialized() { _cascadingModel.StateHasChanged += StateHasChanged; base.OnInitialized(); } private void ChangeGrandValue() { _cascadingModel.Value = DateTime.Now.ToString("o"); } }
在Grand4組件中,我們需要在組件初始化的時候為CascadingModel綁定StateHasChanged事件。效果如下:
5.非嵌套組件間通信
非嵌套組件也就是說在渲染樹中,任一組件無法向上或向下尋找到另外一個組件,例如兄弟組件、叔父組件等。非嵌套組件之間通信,可以通過共同的祖/父組件進行通信,但是這樣設計模式並不友好,因此我們可以利用一個靜態類使用事件訂閱模式進行來進行通信。
靜態類EventDispatcher的定義如下:
public static class EventDispatcher { private static Dictionary<string, Action<object>> _actions; static EventDispatcher() { _actions = new Dictionary<string, Action<object>>(); } public static void AddAction(string key, Action<object> action) { if (!_actions.ContainsKey(key)) { _actions.Add(key, action); } else { throw new Exception($"event key{key} has existed"); } } public static void RemoveAction(string key) { if (_actions.ContainsKey(key)) { _actions.Remove(key); } } public static void Dispatch(string key, object value) { Console.WriteLine("Dispatch"); Console.WriteLine(string.Join(",", _actions.Keys)); if (_actions.ContainsKey(key)) { var act = _actions[key]; act.Invoke(value); } } }
EventDispatcher內部使用一個字典來保存所有的事件,通過AddAction實現事件的注冊,RemoveAction實現事件的移出,Dispatch實現事件的發送。每當初始化一個組件時(OnInitialized),我們使用AddAction注冊一個用於更新本組件內部狀態的事件;在卸載組件時(Dispose),使用RemoveAction將該事件從EventDispatcher中刪除;當其他組件需要更新本組件的內部狀態時,觸發Dispatch即可。
接下來展示一個叔侄之間通信的實例:
Self5.razor
@implements IDisposable <div class="bg-white p-3" style="color: #000;"> <h3>Self5</h3> <span>UpdateNephewValue</span> <input class="w-75" @bind="UncleValue" /> <p>Update From Uncle: @_nephewValue</p> </div> @code { private string _uncleValue = "I'm default uncleValue from nephew"; private string _nephewValue; string UncleValue { get => _uncleValue; set { _uncleValue = value; EventDispatcher.Dispatch("UpdateUncle", _uncleValue); } } protected override void OnInitialized() { EventDispatcher.AddAction("UpdateNephew", (value) => { _nephewValue = (string)value; StateHasChanged(); }); base.OnInitialized(); } protected override void OnAfterRender(bool firstRender) { if (firstRender) { EventDispatcher.Dispatch("UpdateUncle", _uncleValue); } base.OnAfterRender(firstRender); } public void Dispose() { EventDispatcher.RemoveAction("UpdateNephew"); } }
Uncle5.razor
@implements IDisposable <div class="bg-primary jumbotron m-1 text-white"> <h3>Uncle5</h3> <span>UpdateNephewValue</span> <input class="w-75" @bind="NephewValue" /> <p>Update From Nephew: @_uncleValue</p> </div> @code { private string _uncleValue; private string _nephewValue = "I'm default nephew from uncle"; string NephewValue { get => _nephewValue; set { _nephewValue = value; Console.WriteLine("_nephewValue has changed"); EventDispatcher.Dispatch("UpdateNephew", _nephewValue); } } protected override void OnInitialized() { EventDispatcher.AddAction("UpdateUncle", (value) => { _uncleValue = (string)value; StateHasChanged(); }); base.OnInitialized(); } protected override void OnAfterRender(bool firstRender) { if (firstRender) { EventDispatcher.Dispatch("UpdateNephew", _nephewValue); } base.OnAfterRender(firstRender); } public void Dispose() { EventDispatcher.RemoveAction("UpdateUncle"); } }
Parent5.razor
<div class="bg-primary jumbotron m-1 text-white"> <h3>Parent5</h3> <Self5></Self5> </div>
Grand5.Razor
<h3>Grand5</h3> <Parent5 /> <Uncle5/>
在實例中,我們在Self5和Uncle5中都設置了默認值,但是由於兩個組件無法得知另外一個組件OnInitialized是否已經執行,因此在OnAfterRender中在第一次渲染結束后觸發相應的事件,以實現默認值的傳遞。為了在組件銷毀時能移出事件,Self5和Uncle5都還繼承了IDisposable接口。現在效果如下:
代碼:BlazorCrossComponentInterop
本文參考: