0

I am building an editor for a list of objects of related types. My top level shows a list of the objects. Clicking one of them expands it to show its details. Each type of detail is shown in it's own blazor control (e.g. a "NameEditor" for the Name property, which they all have).

QUESTION: How do I update the top level (list) after making a change at the lowest level?

ATM editing is very basic, a button that changes the name to "Reset"

<p>Device.Name in Type1Component.razor = @Device.Name</p> updates as expected

I'm trying to work out how I propogate this update to trigger Type1Component.DeviceChanged when Device.Name changes in NameEditor

Top Level:

@if (topLevel == null){
    <p><em>Loading...</em></p>
} else if (Device == null) {  @*hidden if we are editing a device*@
    <h1>@topLevel.JobRef</h1>

    <h2>@topLevel.Customer</h2>
    <h2>@topLevel.Installation</h2>
    <hr />
    <select class="selectpicker show-tick" multiple title="Filter..." onchange="@Selected">
        @foreach (var item in @deviceTypes) {
            <option>@item</option>
        }
    </select>
    <hr />
    <p>&nbsp;</p>
    <p>@topLevel.Devices.Count() Devices</p>
    foreach (var dev in Devices ) {
        <button onclick="@(() => EditDevice(dev))">@dev.FriendlyDeviceType</button>
        <p>@dev.Name</p>
    }
} else {
    <EditDevice Device="@Device"></EditDevice>
}

@code {
    topLevelRec topLevel;
    //all "devices" inherit DeviceBase
    IEnumerable<DeviceBase> Devices;    //Devices that are shown (filtered from topLevel.Devices)
    IEnumerable<string> deviceTypes;    //filter display by these descriptions
    DeviceBase Device;                  //device we are editing

    protected override async Task OnInitializedAsync(){
        topLevel = await toplevelservice.GettopLevelAsync();
        deviceTypes = topLevel.Devices.Select(d => d.FriendlyDeviceType).Distinct();
        Devices = topLevel.Devices;
    }

    /// <summary>
    /// Apply Filter
    /// </summary>
    /// <param name="c">c.Value is array of selected items to filter to</param>
    protected void Selected(ChangeEventArgs c) {
        if (c.Value == null) {      //no filtering
            Devices = topLevel.Devices;
        } else {
            Devices = topLevel.Devices.Where(d => d.FriendlyDeviceType.IsOneOf<string>((string[])c.Value));
        }
    }

    protected void EditDevice(DeviceBase dev) {
        Device = dev;
        StateHasChanged();
    }

   private void ActionCompleted(EditResult Result) {
        Device = null;    //edit cancelled, go back to showing the list
    }
}

EditDevice component:

<h3>EditDevice</h3>

@if (Device is DeviceBase){
    switch (Device){
        case device_type1 t1:
            <Type1Component @bind-Device=t1></Coupler>
            break;
        case device_type2 t2:
            <Type2Component @bind-Device=t2></Coupler>
            break;
        default:
            <h4>No Editor allocated for @dev.ToString()</h4>
            break;
    }
}
<button onclick="@(() => Cancel())">Cancel</button>

@code {
    [Parameter] public DeviceBase Device { get; set; }
    [Parameter] public EventCallback<EditResult> OnActionCompleted { get; set; }

    private async Task Cancel() {
        await OnActionCompleted.InvokeAsync(EditResult.Cancel);
    }
}

Type1Component:

<h4>@Device.FriendlyName</h4>

<NameEditor @bind-Name=Device.Name></NameEditor>
<p>Device.Name in Type1Component.razor = @Device.Name</p>

<p>Other parameter editors go here...</p>

@code {
    [Parameter] public device_type1 Device { get; set; }
    [Parameter] public EventCallback<device_type1> DeviceChanged { get; set; }
}

NameEditor:

<p><b>@Name</b></p>
<button @onclick=Reset>Reset</button>

@code {
    [Parameter] public string Name { get; set; }
    [Parameter] public EventCallback<string> NameChanged { get; set; }

    async Task Reset() {
        Name = "Reset";
        await NameChanged.InvokeAsync(Name);
    }
}
Eilon
  • 25,582
  • 3
  • 84
  • 102
adrianwadey
  • 1,719
  • 2
  • 11
  • 17

2 Answers2

1

I'm trying to work out how I propogate this update to trigger Type1Component.DeviceChanged when Device.Name changes in NameEditor

Instead of @bind- syntax, use manual event binding:

Type1Component:

<h4>@Device.FriendlyName</h4>

<NameEditor Name=Device.Name NameChanged="OnNameChanged"></NameEditor>
<p>Device.Name in Type1Component.razor = @Device.Name</p>

<p>Other parameter editors go here...</p>

@code {
    [Parameter] public device_type1 Device { get; set; }
    [Parameter] public EventCallback<device_type1> DeviceChanged { get; set; }

    private async Task OnNameChanged(string name)
    {
        Device.Name = name;
        await DeviceChanged.InvokeAsync(Device);
    }
}
Dimitris Maragkos
  • 8,932
  • 2
  • 8
  • 26
1

It depends on the complexity of the component relationships you create.

If this is a simplification of what you really intend then one approach is to get the data out of your UI components and into one or more view services. You then move your notification processes to standard events in the service with components registering handlers and reacting to change events.

There's an example of how to do this in my answer to this question: How can I trigger/refresh my main .RAZOR page from all of its sub-components within that main .RAZOR page when an API call is complete? based on the standard Blazor Template and weather forecasts.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31