How do I pass data between Razor components on the same Blazor page?

Viewed 20822

I have this Blazor page

@page "/bearoffdata"
@using BlazorBoinq.Components

<h3>Bearoff Data</h3>

<Position_Hex_IdPair />

<PositionData />

@code {

}

with these two Razor components:

@using BlazorBoinq.Data
@using BgBearoffCoreNamespace;
@inject BgBearoffService BoService

<label>Position</label>

<input type="text" spellcheck="false" @bind-value="@PositionText" @bind-value:event="oninput" />

<span> = </span>

<input type="number" step="1" @bind-value="@PositionId" @bind-value:event="oninput" />

<label>Id</label>

@code {

    BgBearoffCore BgBo;

    protected override async Task OnInitializedAsync()
    {
        BgBo = await BoService.GetBgBearoffAsync();
    }

    private Int64 positionId;
    private String positionText;

    protected Int64 PositionId
    {
        get => positionId;
        set
        {
            positionId = value;
            if (positionId > 0 && positionId <= BgBo.MaxId)
            {
                positionText = BgBearoffCore.menOnPointToHexString(BgBo.getMenOnPointFromInvariantId(positionId));
            }
            else
                positionText = "";
        }
    }

    protected String PositionText
    {
        get => positionText;
        set
        {
            positionText = value;
            if (BgBo.IsValidHexPosition(positionText))
                positionId = BgBo.getInvariantIdFromPosition(positionText);
            else
                positionId = 0;
        }
    }
}

and

@using BlazorBoinq.Data
@using BgBearoffCoreNamespace;
@inject BgBearoffService BoService

<button class="btn btn-primary" @onclick="ShowBearoffInfo">Show Data</button>

<br>

<textarea cols="36" rows="36" readonly @bind="@BearoffInfo" />


@code {
    BgBearoffCore BgBo;

    protected override async Task OnInitializedAsync()
    {
        BgBo = await BoService.GetBgBearoffAsync();
    }


    private String bearoffInfo = "";
    public String BearoffInfo
    {
        get => bearoffInfo;
        set { }
    }
    protected void ShowBearoffInfo()
    {
        bearoffInfo = BgBo.getPositionInformationText(86);
    }
}

I want to pass the PositionId of the first component to the second component, so I can replace hard-coded 86 in the last line, with the PositionId parameter.

3 Answers

Yeah you have two controls that are not directly related so you can't just simply pass a parameter.

Two options:

Cascading Parameters: https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.0#cascading-values-and-parameters

Or State Management. For State Management, this may help: Implementing State Management In Blazor

You have a class like this:

using System;
public class CounterState
{
    // _currentCount holds the current counter value
    // for the entire application
    private int _currentCount = 0;
    // StateChanged is an event handler other pages
    // can subscribe to 
    public event EventHandler StateChanged;
    // This method will always return the current count
    public int GetCurrentCount()
    {
        return _currentCount;
    }
    // This method will be called to update the current count
    public void SetCurrentCount(int paramCount)
    {
        _currentCount = paramCount;
        StateHasChanged();
    }
    // This method will allow us to reset the current count
    public void ResetCurrentCount()
    {
        _currentCount = 0;
        StateHasChanged();
    }
    private void StateHasChanged()
    {
        // This will update any subscribers
        // that the counter state has changed
        // so they can update themselves
        // and show the current counter value
        StateChanged?.Invoke(this, EventArgs.Empty);
    }
}

You register it in your startup.cs file like this:

services.AddScoped<CounterState>();

You refer to it in each .razor control like this:

@inject CounterState CounterState

One control can set a value like this:

// Call the GetCurrentCount() method
// to get the current count
int CurrentCount = CounterState.GetCurrentCount();
// Increase the count
CurrentCount++;
// Set Current count on the Session State object
CounterState.SetCurrentCount(CurrentCount);

Another control, located anywhere in the application can receive the value like this:

   // This method is called when the control is initialized
    protected override void OnInitialized()
    {
        // Subscribe to the StateChanged EventHandler
        CounterState.StateChanged +=
        OnCounterStateAdvancedStateChanged;
    }
    // This method is fired when the CounterState object
    // invokes its StateHasChanged() method
    // This will cause this control to invoke its own
    // StateHasChanged() method refreshing the page
    // and displaying the updated counter value
    void OnCounterStateAdvancedStateChanged(
        object sender, EventArgs e) => StateHasChanged();
    void IDisposable.Dispose()
    {
        // When this control is disposed of
        // unsubscribe from the StateChanged EventHandler
        CounterState.StateChanged -=
        OnCounterStateAdvancedStateChanged;
    }

Here's one possible solution, that might be feasible because you have access to the code for the components you're using.

There are three steps to this solution:

  1. Define an event callback in your first component.
  2. Define a parameter in your second component.
  3. Define a property in your parent component (your page).

Step 1: Define an event callback in your first component.

This will allow you to notify the parent component (your page) when the property changes.

Declare your PositionId property as a public parameter.

[Parameter] public int PositionId

You can leave your getters and setters as they are.

Change your input to this:

<input type="text" spellcheck="false" @oninput="OnPositionIdChanged" />

Declare an event callback like this:

[Parameter] public EventCallback<int> PositionIdChanged { get; set; }

Then define a method to handle the change like this:

private Task OnPositionIdChanged(ChangeEventArgs e)
{
    PositionId = int.Parse(e.Value.ToString());
    return PositionIdChanged.InvokeAsync(PositionId);
}

Now, when the value in the input changes, an EventCallback will be raised.

Step 2: Define a parameter in your second component.

This will allow you to pass a value into your second component from your parent component (your page).

Declare a public parameter like this:

[Parameter] public int APositionId {get; set; }

Step 3: Define a property in your parent component (your page).

Here, you define a property, get it to update when the value of the property in your first component changes, then supply that value to the parameter in your second component.

Define a property in your page like so:

private int SuppliedPosition { get; set; }

Wire it to the change notifier in your first component like so:

<Position_Hex_IdPair @bind-PositionId="SuppliedPosition"  />

Supply it to the parameter in your second component like so:

<PositionData APositionId="@SuppliedPosition"/>

I've named each of the additional properties slightly differently so it's hopefully clear which is which.

That's it! The drawback to this solution is that it requires you to change your components, and to add code to your page.

There's more information about event callbacks and parameters at the Blazor documentation: Blazor docs.

Hope this helps.

As an alternative you can use Rx.Net.

You can use a service like this.

public interface IThemeMessageService<T>
{
    void SendMessage(ActionMessage<T> message);

    IObservable<ActionMessage<T>> GetMessage();
}

public class ThemeMessageService<T>: IThemeMessageService<T>
{
    private readonly Subject<ActionMessage<T>> _subject = new Subject<ActionMessage<T>>();

    public void SendMessage(ActionMessage<T> message) => _subject.OnNext(message);

    public IObservable<ActionMessage<T>> GetMessage() => _subject;

}

Sending message:

var actionMessage = new ActionMessage<MyData>
{
    Emitter = ThemeMessageEmitter.Component1,
    Data = data
};

ThemeMessageService.SendMessage(actionMessage);

Receving message:

 ThemeMessageService.GetMessage().Subscribe(p =>
 {

     data= p.Data;

 });

Message class:

public class ActionMessage<T>
{
    public ThemeMessageEmitter Emitter { get; set; }
    public T Data { get; set; }
}

Emiter: You can register here the component sending the data

public enum ThemeMessageEmitter
{
    Component1 = 1,
    Component2 = 2,
}

Don't forget to register the service in Startup

 services.AddSingleton(typeof(IThemeMessageService<MyData>), typeof(ThemeMessageService<MyData>));

You can see all in action in my blazor admin theme https://github.com/amuste/BlazorAdminDashboard/tree/master/BlazorAdminDashboard.Client/Shared/Theme

Related