Blazor入門筆記(6)-組件間通訊

  • 2020 年 4 月 10 日
  • 筆記

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 EventDispatch()      {          _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

本文參考:

創建和使用 ASP.NET Core Razor 組件

ASP.NET Core Blazor 事件處理

Tags: