47

I'm using Server-side Blazor components in ASP.NET Core 3 preview 4.

I have a parent component, and child components, using the same shared model, like this :

Model :

public class CountModel
{
    public int Count { get; set; }

    public void Increment()
    {
        Count++;
    }
}

Parent component :

@page "/count"

<CascadingValue Value="currentCount">
    <h1>Count parent</h1>

    <p>Current count is : @currentCount.Count</p>

    <button class="btn btn-primary" onclick="@currentCount.Increment">+1 from parent</button>

    <CountChild></CountChild>
</CascadingValue>

@functions {
    private CountModel currentCount = new CountModel();
}

Child component :

<h1>Count child</h1>

<p>Current count is : @currentCount.Count</p>

<button class="btn btn-primary" onclick="@currentCount.Increment">+1 from child</button>


@functions {
    [CascadingParameter]
    private CountModel currentCount { get; set; }
}

It's the same instance of the model used for the parent and the child. When the model is updated from the parent, both display the correct incremented value. When it's updated from the child, only the child display the correct value.

How can I force the parent component to be refreshed when it is updated from the child ?

Note, here I have a function to update the model, but I would like the solution to work when data is bound to an input.

ArunPratap
  • 4,816
  • 7
  • 25
  • 43
glacasa
  • 1,770
  • 2
  • 16
  • 32
  • 1
    What about to call `StateHasChanged` to notify changes? Like `public void Increment() { Count++; StateHasChanged(); }` Take a look to Chris Sainty post [3 Ways to Communicate Between Components in Blazor](https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/) He uses it. Also related: https://github.com/aspnet/Blazor/issues/420 – dani herrera Apr 20 '19 at 16:30
  • @dani herrera, I don't think the issue is related to not calling StateHasChanged. See my answer above... – enet Apr 20 '19 at 18:04
  • Hi @Issac, I prefer a delegate over CascadingParameter, I guess Chris Sainty cascading sample match exactly this scenario. – dani herrera Apr 20 '19 at 18:27
  • 1
    Yes, I also for delegate... – enet Apr 20 '19 at 18:50
  • I know it is a little bit late but you might want to have a look at this project: https://github.com/dagmanolis/Blaast . It is an implementation of what you are trying to achieve. – Emmanouil Dagdilelis Feb 25 '22 at 20:23

7 Answers7

57

Create a shared service. Subscribe to the service's RefreshRequested event in the parent and Invoke() from the child. In the parent method call StateHasChanged();

public interface IMyService
{
    event Action RefreshRequested;
    void CallRequestRefresh();
 }

public class MyService: IMyService
{
    public event Action RefreshRequested;
    public void CallRequestRefresh()
    {
         RefreshRequested?.Invoke();
    }
}


//child component
MyService.CallRequestRefresh();


//parent component
MyService.RefreshRequested += RefreshMe;

private void RefreshMe()
{
    StateHasChanged();
}
Qudus
  • 1,440
  • 2
  • 13
  • 22
Laurence73
  • 1,182
  • 1
  • 10
  • 14
  • 6
    I ended up doing something similar, but instead of a shared service, the parent component subscribed to the PropertyChanged event – glacasa Apr 23 '19 at 08:36
  • 9
    This worked for me too, but the interface is completely unnecessary. – pajevic Jan 27 '20 at 16:34
  • 4
    The interface is useful to register the service like this; services.AddScoped(); – PBum Aug 20 '20 at 06:53
  • 1
    How to add RefreshRequested if is it async? – Shuvra Nov 10 '20 at 17:23
  • 3
    @Shuvra like this: async () => await RefreshMethod(); – Christopher Bonitz Nov 26 '20 at 12:49
  • 5
    In case anyone else runs into the same issue, make sure your service is not registered as 'Transient' as the parent and child will have their own instances of the service and events won't be triggered between the components. – Even Mien Jun 17 '21 at 13:18
  • How does this affect performance? Am I crazy to wonder why there is no global stateHasChanged method built in, or why it is not global in the first place? – mathkid91 Oct 29 '22 at 16:37
24

Update Parent State by calling it's StateHasChanged method

Create a Method to Update the State on Parent:

public void RefreshState(){
     this.StateHasChanged();
}

Pass the Parent to the Child's by cascading Value or Parameter Example:

<CascadingValue Value="this">
  <ChildComponent /> 
</CascadingValue>

Now on the Child's component declare the Cascading Parameter:

[CascadingParameter]
public ParentPageType _Parent { get; set; }

And Now When you want to refresh the parent just call:

_Parent.RefreshState();
Daniel
  • 2,780
  • 23
  • 21
  • 1. In '[CascadingParameter] public ParentPageType _Parent { get; set; }' What does the ParentPageType stand for? 2. _Parent.RefreshState(); is then called from the Child, right? – nogood Mar 30 '21 at 09:54
  • 1
    ParentPageType _Parent is Set by the Cascading Parameter (Auto set) and it is the Page Component that created the child component. _Parent.RefreshState(); is called from the child. – Daniel Apr 05 '21 at 11:14
  • 1
    Just info for Telerik Blazor TelerikWindow component; this CascadingValue will be null in child components since Telerik Window renders at the root of the application, so its contents go out of the context of the original CascadingValue component. This is their solution https://docs.telerik.com/blazor-ui/knowledge-base/window-cascading-parameter-null – kite Aug 29 '21 at 09:48
10

The following code snippet is the most appropriate method to refresh a parent component when a model is updated from its child component. But it adds more to the bargains: no dependency between parent and child. It is not specifically created to notify of a state change. It notifies when a property, any property has changed, and it can provides to subscribers the name of the property whose value has changed, the new value, etc.

 using System.ComponentModel;
 using System.Runtime.CompilerServices;
 using System.ComponentModel.DataAnnotations;

The main point to note here is that our model class implements the INotifyPropertyChanged interface...

CountModel.cs

public class CountModel : INotifyPropertyChanged
{
    private int count;
    public int Count
    {
        get => count;
        set => SetProperty(ref count, value);
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new 
                                   PropertyChangedEventArgs(propertyName));
    }

    bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string 
                                                     propertyName = null)
    {
        if (Equals(storage, value))
        {
            return false;
        }

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    public void Increment()
    {
        Count++;
    }
}

Count.razor

@page "/count"
@implements IDisposable

<CascadingValue Value="currentCount">
    <h1>Count parent</h1>

    <p>Current count is : @currentCount.Count</p>

    <button class="btn btn-primary" @onclick="@currentCount.Increment">+1 
                                                     from parent</button>

    <CountChild></CountChild>
</CascadingValue>

@code {
    private CountModel currentCount = new CountModel();

    protected override void OnInitialized()
    {
       currentCount.PropertyChanged += (sender, args) => StateHasChanged();
    }

    public void Dispose()
    {
        currentCount.PropertyChanged -= (sender, args) => StateHasChanged();
    }
}

CountChild.razor

<h1>Count child</h1>

<p>Current count is : @currentCount.Count</p>

<button class="btn btn-primary" @onclick="@currentCount.Increment">+1 from 
                                                            child</button>


@code {
     [CascadingParameter]
     private CountModel currentCount { get; set; }


}

Hope this helps...

enet
  • 41,195
  • 5
  • 76
  • 113
8

The flow of Cascading parameters is downwards. For your parent to be refreshed, you want to provide a callback that the child component can call, passing it some value. I've already shown in the Blazor section here how to create a callback on the parent component, and how to trigger the callback, passing it a value.

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
enet
  • 41,195
  • 5
  • 76
  • 113
  • 4
    Thanks Issac, after some tests, I prefer Laurence73's solution, because when I use a service I don't have direct dependence between the parent and its children, and it adds more flexibility (for exemple, it's easier to handle siblings components) – glacasa Apr 23 '19 at 08:39
2

A parent component re-renders if a child component invokes an EventCallback that the parent provides. So technically, you can do this:

  • In the child component, add the following:
[Parameter]
public EventCallback OnIncrement { get; set; }
  • Then invoke it whenever you increment (pseudo code bellow):
<button @onClick=@(args => HandleClick()) ... >
Task HandleClick()
{
    currentCount.Increment();

    return OnIncrement.InvokeAsync();
}
  • In the parent component, provide the OnIncrement callback:
<CountChild OnIncrement=@( () => {} )/>

Even if the provided callback does nothing, it's invocation triggers a re-render.

Be aware that having methods/functions that "do nothing" is generally considered a code smell.

Abdelhakim
  • 815
  • 1
  • 5
  • 19
  • _A parent component re-renders if a child component invokes an EventCallback that the parent provides._ Can you provide reference for this? It seems to be working, even from code, like `EventCallback.Factory.Create(this, EventCallback.Empty)`. – Attila Apr 07 '22 at 17:34
  • 1
    @Attila [Here is a link to the doc](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/rendering?view=aspnetcore-6.0#rendering-conventions-for-componentbase). It says that rerender is triggered ```After notification of an event```. And this is how notification is done in blazor: the child component notifies a parent component of an event by invoking the event callback that the parent provides. – Abdelhakim Apr 08 '22 at 06:37
1

Binding might be another way that might make it easier. What you can do is have a [Parameter] (as opposed to a [CascadingParameter]) that's an object. You also declare a Changed EventCallback<typeof(SharedClass)>

[Parameter]
public InputLength Model { get; set; }
[Parameter]
public EventCallback<InputLength> ModelChanged { get; set; }

Then on the parent in your class declaration you @bind-=Instance of the SharedObject.

 <InputLength @bind-Model=inputLength></InputLength>

The final step is to call Changed.InvokeAsync(object w/ updated data). Where this is done is depended on what actually updates the shared values.

public double Length;
private double _Length
{
    get { return Model.Length; }
    set { Model.Length = value; ModelChanged.InvokeAsync(Model); }
}

That call will trigger a refresh of the parents object and update the UI as necessary.

misterbee180
  • 325
  • 1
  • 13
0

This is how you can achieve functionality your are looking for

Here is Model, it has a Action which parent component will subscribe to, to get changes made by child component

 public class CountModel
 {
    public int Count { get; set; }    
    public Action Changed;
    
    public void Increment()
    {
       Count++;
       Changed.Invoke();
    }
 }

This is parent component, parent component has to subscribe to Model to get changes in count

@page "/count"

<CascadingValue Value="currentCount">
    <h1>Count parent</h1>

    <p>Current count is : @currentCount.Count</p>

    <button class="btn btn-primary" onclick="@(()=>currentCount.Increment())">+1 from parent</button>

    <Child></Child>
</CascadingValue>

@code {
    private CountModel currentCount = new CountModel();    

    protected override void OnInitialized()
    {
        currentCount.Changed = StateHasChanged;
    }        
}

and here is Child component

<h1>Count child</h1>

<p>Current count is : @currentCount.Count</p>

<button class="btn btn-primary" onclick="@(()=>currentCount.Increment())">+1 from child</button>    

@code {
    [CascadingParameter]
    private CountModel currentCount { get; set; }
}
Surinder Singh
  • 1,165
  • 1
  • 3
  • 11