在使用 Blazor 时,避免不了要进行组件间通信,组件间的通信大致上有以下几种:

(1) 父、子组件间通信;

(2) 多级组件组件通信,例如祖、孙节点间通信;

(3) 非嵌套组件间通信。

# 父、子组件间通信

父、子组件间通信分为两类:父与子通信、子与父通信。

# 父与子通信

父与子通信是最为简单的,直接通过数据绑定即可实现:

Self1.razor

r
<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

r
<div class="bg-primary jumbotron  text-white">
    <h3>Parent1</h3>
    <Self1 Value="I'm from Parent1"></Self1>
</div

效果如下:

# 子与父通信

子与父通信是通过回调事件实现的,通过将事件函数传递到子组件,子组件在数据发生变化时,调用该回调函数即可。在 Self1.razor 和 Parent1.razor 组件上进行修改,为 Self1 组件基础上添加一个事件 OnValueChanged,并在数据 Value 发生变化时执行该事件,通知父组件新数据是什么,在这里,我没有在子组件中更新 Value 的值,因为新的数据会从父组件流到子组件中。现在的得到的组件 Self2.razor 和 Parent2.razor 的代码如下:

Self2.razor

r
<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

r
<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;
    }
}

效果如下:

# 使用 @bind

@bind 支持数据的双向绑定,但是当子组件发生变化时,依然需要调用回调事件,不过好处就是回调事件不用你写。

# 祖、孙组件间通信

祖、孙组件间的通信也分为两类:祖与孙通信、孙与祖通信。最暴力的方法就是通过父节点中转,实现祖 - 父 - 孙通信,但是当跨越多个层级的时候就比较麻烦,好在 Blazor 提供了 “Cascading values and parameters”,中文翻译为级联值和参数。级联值和参数是通过 CascadingValue 组件和 CascadingParameter 属性注解实现的。

# 祖与孙通信

Self3.razor

r
<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

r
<div class="bg-primary jumbotron text-white">
    <h3>Parent3</h3>
    <Self3></Self3>
</div>

Grand3.Razor

r
<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 接收该参数即可。

# 孙与祖通信

孙与祖通信与子与父通信一样,需要使用事件进行回调,这个回调方法也是一个参数,因此只需要将该回调也通过 CascadingValue 传递到孙组件中,当孙组件数据发生变化时调用该回调函数即可。传递的方法如 4.1. 所示有两种,但是无论哪种都需要在祖组件中手动调用 StateHasChanged。另外,如果直接更新值或者引用,请不要在孙组件中直接更新,只需要调用回掉即可,因为会触发两次渲染(可以在代码的 GrandX 看到)。当然,如果是引用中的值,比如 model 中的值,是需要在子组件中更新的。 这里我们将参数和回调封装成一个类:

p
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

r
<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

r
<div class="bg-primary jumbotron text-white">
    <h3>Parent4</h3>
    <Self4></Self4>
</div>

Grand4.razor

r
<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 事件。效果如下:

# 非嵌套组件间通信

非嵌套组件也就是说在渲染树中,任一组件无法向上或向下寻找到另外一个组件,例如兄弟组件、叔父组件等。非嵌套组件之间通信,可以通过共同的祖 / 父组件进行通信,但是这样设计模式并不友好,因此我们可以利用一个静态类使用事件订阅模式进行来进行通信。

静态类 EventDispatcher 的定义如下:

p
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

r
@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

r
@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

r
<div class="bg-primary jumbotron m-1 text-white">
    <h3>Parent5</h3>
    <Self5></Self5>
</div>

Grand5.Razor

r
<h3>Grand5</h3>
<Parent5 />
<Uncle5/>

在实例中,我们在 Self5 和 Uncle5 中都设置了默认值,但是由于两个组件无法得知另外一个组件 OnInitialized 是否已经执行,因此在 OnAfterRender 中在第一次渲染结束后触发相应的事件,以实现默认值的传递。为了在组件销毁时能移出事件,Self5 和 Uncle5 都还继承了 IDisposable 接口。现在效果如下:

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Roc 微信支付

微信支付

Roc 支付宝

支付宝