如何在两个兄弟 Blazor 组件之间通信?

5

我有一个Blazor页面,其中包含两个组件。其中一个组件有一个按钮,当单击它时会生成一个随机数。另一个组件有一个文本区域,应该显示生成的随机数。

<h1>Parent Page</h1>

<ProvideNumberComponent />

<DisplayNumberComponent  />

@code {
}

<h3>Provides Number</h3>

<button class="btn btn-primary" @onclick="CalculateNumber">Provide Number</button>

@code {
    private void CalculateNumber(MouseEventArgs e)
    {
        Random rnd = new Random();
        Int32 nextNumber = rnd.Next();
    }

}

<h3>Displays number</h3>

<textarea cols="9" rows="1" readonly style="font-family:monospace;" />

@code {

}

如何在显示相邻组件中最清晰地获取计算相邻组件中的数字?

我的代码存在问题,即每次单击按钮都会实例化一个Random对象,而不是在初始化时仅实例化一次。最好的解决方案是将Random对象放在单例服务类中,并将其注入到计算组件中吗?

5个回答

4

在我看来,最好的解决方案是创建一个实现状态模式和通知器模式的服务。以下代码描述了如何通过中介者在两个兄弟节点之间进行通信。

NotifierService.cs

-->

通知器服务.cs

public class NotifierService
{
    public NotifierService()
    {

    }

    int rnd;
    public int RandomNumber
    {
        get => rnd;
        set
        {
            if (rnd != value)
            {
                rnd= value;

                if (Notify != null)
                {
                    Notify?.Invoke();
                }
            }
        }
     }
     public event Func<Task> Notify;
 }

请添加以下代码: services.AddScoped<NotifierService>();

ProvideNumberComponent.razor

 @inject NotifierService Notifier
 @implements IDisposable

<h3>Provides Number</h3>

 <button class="btn btn-primary" @onclick="CalculateNumber">Provide 
                                                    Number</button>

 @code 
 {
    private void CalculateNumber(MouseEventArgs e)
   {
      Random rnd = new Random();
      Int32 nextNumber = rnd.Next();

      Notifier.RandomNumber = nextNumber; 
   }

   public async Task OnNotify()
   {
    await InvokeAsync(() =>
    {
        StateHasChanged();
    });
  }


 protected override void OnInitialized()
 {
    Notifier.Notify += OnNotify;
 }


 public void Dispose()
 {
    Notifier.Notify -= OnNotify;
 }

}

DisplayNumberComponent.cs

 @inject NotifierService Notifier
 @implements IDisposable

 <hr />
<h3>Displays number</h3>

<textarea cols="9" rows="1" readonly style="font-family:monospace;">
    @Notifier.RandomNumber
</textarea>

@code {

    public async Task OnNotify()
   {
    await InvokeAsync(() =>
    {
        StateHasChanged();
    });
  }


 protected override void OnInitialized()
 {
    Notifier.Notify += OnNotify;
 }


 public void Dispose()
 {
    Notifier.Notify -= OnNotify;
 }

 }

当然,您可以在多个组件中注入并使用该服务,还可以添加更多功能以提供该服务。通过事件处理程序实现通信可能会有问题,除非它是在父级和其子级之间进行的...
希望这个可行...

谢谢!在DisplayNumberComponent中删除textarea标签的'value'属性后,我成功让你的代码运行了。 - Hal Heinrich

2

确实有很多方法可以实现您的目标,我只想向您展示我更喜欢的方法:

父组件:

<EditForm Model="Message">
    <PageOne @bind-Send="Message.Text"/>
    <PageTwo @bind-Receive="Message.Text"/>
</EditForm>

@code{
    public Content Message { get; set; }=new Index.Content();

    public class Content
    {
        public string Text { get; set; } = "Hello world";
    }
}

PageOne 组件 - 发送值的组件:

    <button @onclick="@GetGuid">Change value</button>

@code{
    [Parameter] public string Send { get; set; }
    [Parameter] public EventCallback<string> SendChanged { get; set; }

   async void GetGuid()
    {
       await SendChanged.InvokeAsync(Guid.NewGuid().ToString());
    }
}

PageTwo是接收数据的组件

<h1>@Receive</h1>

@code{
    [Parameter] public string Receive { get; set; }
    [Parameter] public EventCallback<string> ReceiveChanged { get; set; }
}

说明:

通常情况下,我们需要第三方服务进行通信,在这种情况下,我使用了EditForm组件,它可以存储一个Model,并且此模型的属性可以被所有子组件共享。

我还创建了一个自定义组件,功能较少,名为PhoneBox(用于替代EditForm),只是为了明确其作用:)

PhoneBox-第三方通信服务:)

<CascadingValue Value="EditContext">
    @ChildContent(EditContext)
</CascadingValue>

@code {
    [Parameter] public object Model { get; set; }
    [Parameter]public EditContext EditContext { get; set; }
    [Parameter] public RenderFragment<EditContext> ChildContent { get; set; }

    protected override void OnInitialized()
    {
        EditContext = new EditContext(Model);
    }
}

我更喜欢这种方法,因为它看起来更符合 "blazor way" 的风格 :)

看看 "blazor way" 多么好。

<PhoneBox Model="Message">
    <PageOne @bind-Send="Message.Text"/>
    <PageTwo @bind-Receive="Message.Text"/>
</PhoneBox>

您可以查看一个实际工作的例子:工作示例

我忘了提到,PhoneBox可以在任何需要时重复使用,使用任何类型的数据 :) - Lucian Bumb
我实现了你的代码,但在你的父组件中出现了以下编译错误:无法从绑定属性“bind-Send”推断属性名称。绑定属性应该是形式为“bind”或“bind-value”的形式,以及它们相应的可选参数,如“bind-value:event”,“bind:format”等。 在 <PageOne @bind-Send="Message.Text" />类似的错误也发生在 在 <PageTwo @bind-Receive="Message.Text" /> - Hal Heinrich
我也遇到了错误CS0426:“类型名称'Content'在类型'Index'中不存在” 在 public Content Message { get; set; } = new Index.Content();我通过重写该行来修复了最后一个错误: public Content Message { get; set; } = new Content(); - Hal Heinrich
你有没有查看BlazorFiddle上的工作示例?如果组件中没有声明两个参数,一个用于Send,另一个用于SendChanged,那么会出现关于bind-Send的错误。 - Lucian Bumb
我确实检查了BlazorFiddle,并且可以看到它可以工作。但是当我将那段代码放入Visual Studio创建的默认解决方案中时,我会得到之前描述的编译错误。我的参数声明看起来和你的一样。 - Hal Heinrich
如果您在标记中调用组件时未按其确切名称命名组件,则可能会出现此错误。组件的名称区分大小写,例如,如果您将组件命名为Pageone,并且在标记中使用<PageOne @bind-...,则会出现您所说的错误,因为编译器无法找到正确的组件。 - Lucian Bumb

0

0

我认为接口是实现这一目标的最佳方式。

这来自我的Nuget包,DataJugger.Blazor.Components

接口IBlazorComponent:

#region using statements

using System.Collections.Generic;

#endregion

namespace DataJuggler.Blazor.Components.Interfaces
{

    #region interface IBlazorComponent
    /// <summary>
    /// This interface allows communication between a blazor componetn and a parent component or page.
    /// </summary>
    public interface IBlazorComponent
    {

        #region Methods

            #region ReceiveData(Message message)
            /// <summary>
            /// This method is used to send data from a child component to the parent component or page.
            /// </summary>
            /// <param name="data"></param>
            void ReceiveData(Message message);
            #endregion

        #endregion

        #region Properties

            #region Name
            /// <summary>
            /// This property gets or sets the Name.
            /// </summary>
            public string Name { get; set; }
            #endregion

            #region Parent
            /// <summary>
            /// This property gets or sets the Parent componet or page for this object.
            /// </summary>
            public IBlazorComponentParent Parent { get; set; }
            #endregion

        #endregion

    }
    #endregion

}

IBlazorComponentParent接口

#region using statements

using System.Collections.Generic;

#endregion

namespace DataJuggler.Blazor.Components.Interfaces
{

    #region interface IBlazorComponentParent
    /// <summary>
    /// This interface is used to host IBlazorComponent objects
    /// </summary>
    public interface IBlazorComponentParent
    {

        #region Methods

            #region FindChildByName(string name)
            /// <summary>
            /// This method is used to find a child component that has registered with the parent.
            /// </summary>
            /// <param name="name"></param>
            /// <returns></returns>
            IBlazorComponent FindChildByName(string name);
            #endregion

            #region ReceiveData(Message message)
            /// <summary>
            /// This method is used to send data from a child component to the parent component or page.
            /// </summary>
            /// <param name="data"></param>
            void ReceiveData(Message message);
            #endregion

            #region Refresh()
            /// <summary>
            /// This method will call StateHasChanged to refresh the UI
            /// </summary>
            void Refresh();
            #endregion

            #region Register(IBlazorComponent component)
            /// <summary>
            /// This method is called by the Sprite to a subscriber so it can register with the subscriber, and 
            /// receiver events after that.
            /// </summary>
            void Register(IBlazorComponent component);

        #endregion

        #endregion

        #region Properties

            #region Children
            /// <summary>
            /// This property gets or sets the value for Children.
            /// </summary>
            public List<IBlazorComponent> Children { get; set; }
            #endregion

        #endregion

    }
    #endregion

}

关于使用,以下是最相关的部分:

在您的组件中,它是一个IBlazorCompoent(子级),其中有一个Parent属性。

在您的组件中,您可以像这样设置父级:

<Login Parent=this></Login>

然后在你的组件中,你可以像这样修改父级属性:

[Parameter]
public IBlazorComponentParent Parent
{
    get { return parent; }
    set 
    { 
        // set the value
        parent = value;

        // if the Parent exists
        (Parent != null)
        {
            // Register with the parent
            Parent.Register(this);
        }
    }   
}

接下来,在实现了 IBlazorComponentParent 接口的父组件中,添加一个用于存储你的组件的属性,并将 Register 方法改为以下代码:
// Login component reference
public Login LoginComponent { get; set; }


public void Register(IBlazorComponent component)
{
    if (component is Login)
    {
        // Store the LoginComponent
        LoginComponent = component as Login;
    }
    else if (component is Join)
    {
        // Store the compoent
        SignUpComponent = component as Join;
    }
}

此时,我的登录组件知道其父组件,而其父组件也知道登录组件,因此我可以这样发送消息:

从子组件中,发送一个简单的消息:

if (Parent != null)
{
    Message message = new Message();

    message.Text = "Some message";

    Parent.SendMessage(message);
}

或者发送一个复杂的消息

// create a message
DataJuggler.Blazor.Components.Message message = new DataJuggler.Blazor.Components.Message();

// Create the parameters to pass to the component
NamedParameter parameter = new NamedParameter();

// Set the name
parameter.Name = "PixelInformation Update";
parameter.Value = pixel;

// Create a new collection of 'NamedParameter' objects.
message.Parameters = new List<NamedParameter>();

// Add this parameter
message.Parameters.Add(parameter);

// Send this to the component
ColorPickerComponent.ReceiveData(message);

然后在父级中接收消息:

public void ReceiveData(Message message)
{
    // If the message object exists and has parameters
    if ((message != null) && (message.HasParameters))
    {
        // if this a PixelInformation update from the Index page
        if (message.Parameters[0].Name == "PixelInformation Update")
        {
            // this is only relevant to my app, just showing an example of 
            // \what I do with the data after it is received.

            // Set the SelectedPixel
            SelectedPixel = (PixelInformation) message.Parameters[0].Value;

            // Set the properties from the Pixel to display
            SetupColorPicker();
        }
    }
}

上述代码用于我的最新网站PixelDatabase.Net https://pixeldatabase.net

如果有人需要,Nuget包的代码都是开源的:

DataJuggler.Blazor.Components https://github.com/DataJuggler/DataJuggler.Blazor.Components

我来自Windows Forms背景,所以我喜欢能够像这样在组件之间进行通信,而数据绑定并不总是有效。

this.Login.DoSomething(data); 

你也可以像这样将父类转换为特定类型:
public IndexPage ParentIndexPage
{
    get
    {
        // cast the Parent object as an Index page
        return this.Parent as IndexPage;
    }
}

因此,如果父级存在,您的子级可以调用父级的方法或设置属性,因此始终要添加:

public bool HasParentIndexPage
{
    get
    {
        // return true if the ParentIndexPage exists
        return (ParentIndexPage != null);
    }
}

因此,为了方便子级使用:

// if the parent index page exists
if (HasParentIndexPage)
{
    // Safely call your parent page
    ParentIndexPage.SomeMethod();
}

谢谢您的帖子,但对于我想要的有点过多了。 - Hal Heinrich

0
一种实现方式是使用会话模式,并在两个组件中注入相同的实例,然后在更改时通知它们。另一种更快的方式可能是使用双向绑定和事件回调。 在 ProvideNumberComponent.razor 中
<button class="btn btn-primary" @onclick="CalculateNumber">Provide Number</button>

@code {

    [Parameter]
    public EventCallback<int> OnRandomNumberSet{get; set;}
    private void CalculateNumber(MouseEventArgs e)
    {
        Random rnd = new Random();
        Int32 nextNumber = rnd.Next();
        OnRandomNumberSet.InvokeAsync(nextNumber);
    }

}

ParentComponent.razor 文件中

<h1>Parent Page</h1>

<ProvideNumberComponent OnRandomNumberSet="((r) => SetRandomNumber(r))"/>

<DisplayNumberComponent TextAreaValue="_randomNumber" />

@code {
   private int _randomNumber;
   private void SetRandomNumber(int randomNumber)
   {
       _randomNumber = randomNumber;
   }
}

在DisplayNumberComponent.razor文件中
<h3>Displays number</h3>
<textarea cols="9" rows="1" bind:value="TextAreaValue" readonly style="font-family:monospace;" />
@code 
{
  [Parameter]
  public int TextAreaValue{get; set;}
}

我实现了你的代码,但是当我点击按钮时,显示组件文本区域中没有任何内容。我在页面父级和提供组件中设置了断点,它们都被触发并且表现正常。有什么想法吗? - Hal Heinrich
@HalHeinrich 在父组件中添加 Statehaschanged()。 - LordSilvermort
我在父页面的SetRandomNumber(int randomNumber)方法末尾添加了StateHasChanged();。它确实被执行了,但是显示组件上没有任何内容。 - Hal Heinrich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接