Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to access parent component from child component. #15651

Closed
albiberto opened this issue Aug 23, 2018 · 12 comments
Closed

How to access parent component from child component. #15651

albiberto opened this issue Aug 23, 2018 · 12 comments
Labels
area-blazor Includes: Blazor, Razor Components

Comments

@albiberto
Copy link

I need to do something very similar to what is described in https://blazor.net/docs/components/index.html,
section [Data Binding], subsection [Component Parameters], with the only exception that I need to update the parent component from the child, so I first moved the button to the latter. The implementation is:

ParentComponent

@page "/ParentComponent"

<h1>Parent Component</h1>

<p>ParentYear: @ParentYear</p>

<ChildComponent bind-Year="@ParentYear" />

@functions {
    [Parameter]
    private int ParentYear { get; set; } = 1978;
}

ChildComponent

<h2>Child Component</h2>
<p>Year: @Year</p>

<button class="btn btn-primary" onclick="@ChangeTheYear">Change Year to 1986</button>

@functions {
    [Parameter]
    private int Year { get; set; }

    [Parameter]
    private Action<int> YearChanged { get; set; }

    void ChangeTheYear()
    {
        Year = 1986;
    }
}

But this approach doesn't work. So I tried to bind the event and value directly (see the code below), but it still fails, the only improvement is that now I can see the "I was called" line in the console output.

ParentComponent

@page "/ParentComponent"

<h1>Parent Component</h1>

<p>ParentYear: @ParentYear</p>

<ChildComponent Year="@ParentYear"  YearChanged="@((e) => { Console.WriteLine("I was called"); @ParentYear = e;})"/>

@functions {
    [Parameter]
    private int ParentYear { get; set; } = 1978;
}

ChildComponent

<h2>Child Component</h2>

<p>Year: @Year</p>

<button class="btn btn-primary" onclick="@ChangeTheYear">Change Year to 1986</button>

@functions {
    [Parameter]
    private int Year { get; set; }

    [Parameter]
    private Action<int> YearChanged { get; set; }

    void ChangeTheYear()
    {
        Year = 1986;
        YearChanged(Year);
    }
}

Then I added manualy UI refresh

ParentComponent

@page "/ParentComponent"

<h1>Parent Component</h1>

<p>ParentYear: @ParentYear</p>

<ChildComponent Year="@ParentYear" YearChanged="@((e) =>
                                   {
                                       Console.WriteLine("I was called!");
                                       @ParentYear = e;
                                       StateHasChanged();
                                   })"></ChildComponent>

@functions {
    [Parameter]
    private int ParentYear { get; set; } = 1978;
}

Child Component

<h2>Child Component</h2>

<p>Year: @Year</p>

<button class="btn btn-primary" onclick="@ChangeTheYear">Change Year to 1986</button>

@functions {
    [Parameter]
    private int Year { get; set; }

    [Parameter]
    private Action<int> YearChanged { get; set; }

    void ChangeTheYear()
    {
        Year = 1986;
        YearChanged(Year);
    }
}

The last thing I tried was to add a manual UI refresh via StateHasChanged(), this works, but I don't understand why the forced refresh is necessary, as the bind-{property} syntax should perform a Two-Way binding already. Is this behaviour intended or is it a bug?

Thanks and regards,
Alberto

@Lupusa87
Copy link
Contributor

If you want parent to get notified about child change or child to do something with parent, you can do it with different ways:

1. Child will have action and parent will subscribe on it, child can invoke this action when we need parent to notify, parent subscribed callback will fire and will do parent side stuff.
We need to capture reference for child and subscribe for event, It is important to do this inside OnAfterRender because before this reference will be null (for example inside OnInit).

@page "/"

<h1>Parent Component</h1>

<p>ParentYear: @ParentYear</p>

<ChildComponent ref="ChildComponent1" bind-Year="@ParentYear" />

@functions {
    private int ParentYear = 1978;

    ChildComponent ChildComponent1 = new ChildComponent();

    protected override void OnAfterRender()
    {
        ChildComponent1.YearChanged += ChildFiredEvent;  
    }

    public void ChildFiredEvent(int _year)
    {
        ParentYear = _year;
        StateHasChanged();
    }

}
<h2>Child Component</h2>

<p>ChildYear: @Year</p>

<button class="btn btn-primary" onclick="@ChangeTheYear">Change Year to 1986</button>

@functions {
    [Parameter]
    private int Year { get; set; }

    [Parameter]
    public Action<int> YearChanged { get; set; }

    void ChangeTheYear()
    {
        Year = 1986;
        YearChanged?.Invoke(Year);
    }
}

2. You can pass parent instance reference as parameter to child or you can save parent instance reference in some static class variable and then access and manipulate it from child.
You can get parent instance reference inside OnInit with keyword this and pass as parameter or save in static class variable.

In my opinion first approach is better if you have small to do with parent, second one is better if you have lot to do with parent and from different places inside or outside child component.

Using static variables is often helpful and I do it, you can access them from anywhere you like, you can bind component properties to it and get components refreshed with calling StateHasChanged from outside with saved instance reference. (if it is confusing I can show how it works). But it has some drawbacks too.
This repo is using this kind of things.

We can use different approaches to achieve our goals, with one will be qualified as best practice will show future.

Hope this was helpful, good luck with your coding.

@albiberto
Copy link
Author

albiberto commented Aug 24, 2018

Thank you very muck.

@Lupusa87
Copy link
Contributor

You are welcome :)

I think will be good if we change issue name another people to find it useful too.

My suggestion will be - How to access parent component from child component.

@albiberto albiberto changed the title Component parameters unexpected behavior How to access parent component from child component. Aug 24, 2018
@albiberto
Copy link
Author

👯‍♂️

@Lupusa87
Copy link
Contributor

You can ask questions here for more quick replies.

@albiberto
Copy link
Author

albiberto commented Aug 27, 2018

I tried to implement your proposed solution (approach 1), along with two solutions based on your suggestions (approaches 2A and 2B) and my original solution (approach 3).

Approach 1

ChildComponent

<h2>Child Component</h2>

<input type="button" value="Previuos" onclick="@Previous"/>
<input type="button" value="Next" onclick="@Next"/>

@functions {
    [Parameter]
    private int Counter { get; set; }

    [Parameter]
    public Action<int> CounterChanged { get; set; }

    void Next()
    {
        ++Counter;
        CounterChanged?.Invoke(Counter);
    }   
    
    void Previous()
    {
        --Counter;
        CounterChanged?.Invoke(Counter);
    }
}

ParentComponent

@page "/"

<h1>Parent Component</h1>

<p>Counter: @Counter</p>

<Child ref="ChildComponent1" bind-Counter="@Counter" />

@functions {
    private int Counter = 1;

    Child ChildComponent1 = new Child();

    protected override void OnAfterRender()
    {
        ChildComponent1.CounterChanged += ChildFiredEvent;  
    }

    public void ChildFiredEvent(int counter)
    {
        Counter = counter;
        StateHasChanged();
    }
}

This code generate a warning due the event CounterChanged being public.

warning BL9993: Component parameter 'CounterChanged' is marked public, but component parameters should not be public.

Approach 2A

Based on you suggestions, I coded a first implementation

ChildComponent

<h2>Child Component</h2>

<input type="button" value="Previuos" onclick="@Previous"/>
<input type="button" value="Next" onclick="@Next"/>

@functions {
    [Parameter]
    ParentComponent Parent { get; set; }

    void Next()
    {
        ++Parent.Counter;
    }

    void Previous()
    {
        --Parent.Counter;
    }
}

ParentComponent

@page "/"

@using Microsoft.AspNetCore.Blazor

<h1>Parent Component</h1>

Counter: @Counter

<Child Parent="@this"></Child>


@functions {

    int _counter = 1;

    public int Counter
    {
        get => _counter;
        set
        {
            _counter = value;
            StateHasChanged();
        }
    }
}

This implementation has a problem: Parent type is fixed for the component, that make the code not reusable.
For this reason I rewrote the code with the intent to made it reusable.

Approach 2B

ChildBase

public abstract class ChildBase : BlazorComponent
{
	int _counter = 1;

	public int Counter
	{
		get => _counter;
		set
		{
			_counter = value;
			StateHasChanged();
		}
	}
}

ChildComponent

<h2>Child Component</h2>

<input type="button" value="Previuos" onclick="@Previous"/>
<input type="button" value="Next" onclick="@Next"/>

@functions {
    [Parameter]
    ChildBase Parent { get; set; }

    void Next()
    {
        ++Parent.Counter;
    }

    void Previous()
    {
        --Parent.Counter;
    }
}

ParentComponent

@page "/"

@inherits FromChildToParentWireless.Pages.ChildBase

<h1>Parent Component</h1>

Counter: @Counter

<Child Parent="@this"></Child>

Approach 3

This is the solution using a property and an event both passed to the Child as parameter.

ChildComponent

<h2>Child Component</h2>

<input type="button" value="Previuos" onclick="@Previous"/>
<input type="button" value="Next" onclick="@Next"/>

@functions {
    [Parameter]
    private int Counter { get; set; }

    [Parameter]
    Action<int> CounterChanged { get; set; }

    void Next()
    {
        CounterChanged(++Counter);
    }   
    
    void Previous()
    {
        CounterChanged(--Counter);
    }
}

ParentComponent

@page "/"

<h1>Parent Component</h1>

<p>Counter: @Counter</p>

<Child  Counter="@Counter" CounterChanged="@ChildFiredEvent"/>

@functions {
    private int Counter = 1;

    void ChildFiredEvent(int counter)
    {
        Counter = counter;
        StateHasChanged();
    }
}

Conclusions:

I prefer to avoid having warnings in my code, so I have to exclude approach 1.
I also prefer to have my code much reusable as possible, therefore I have to exclude solution 2A.
This leaves approach 2B and 3, but I prefer approach 3 since it uses events like bind-{Property}.
What I wanted to know was if the call to StateHasChanged() is mandatory for a reason, even when a Two-Way binding is implemented via bind-{property},
or if it's a bug and bind-{property} should be enough to refresh the page.

@albiberto albiberto reopened this Aug 27, 2018
@SteveSandersonMS
Copy link
Member

What I wanted to know was if the call to StateHasChanged() is mandatory for a reason, even when a Two-Way binding is implemented via bind-{property},

It's a known issue (#610) that will eventually be fixed.

@Lupusa87
Copy link
Contributor

Sorry I was not able to reply, now also have not time but Sunday evening I will show my samples for this scenario.
My recommended way 2 works differently.
Let me show you later or check this repos:
https://github.com/Lupusa87/BlazorGameSnake
https://github.com/Lupusa87/BlazorChess

@Lupusa87
Copy link
Contributor

Lupusa87 commented Sep 3, 2018

Warning that component property or action is public not makes sense and should be removed because we often split markup and logic, so if we not make properties or actions public markup can't see them!

Hope team will remove this warning or tell us how can be solved above mentioned case.
@SteveSandersonMS
So I think you should not worry about this warning.

If no one will reply I will open separate issue about this no sense warning.

Another part needs more time to answer, maybe later :)

@SteveSandersonMS
Copy link
Member

Warning that component property or action is public not makes sense and should be removed because we often split markup and logic, so if we not make properties or actions public markup can't see them!

Use protected if your goal is to access the property in a subclass.

@Lupusa87
Copy link
Contributor

Lupusa87 commented Sep 5, 2018

Protected helps in this case but there are another cases too where it is not helpful, I opened new Issue - 1388.

Thank you for answer.

@Lupusa87
Copy link
Contributor

Lupusa87 commented Sep 5, 2018

@aviezzi
I often save component instance and then invoke different methods, actions or change properties as I need.

For example:
I am setting in some class's property this component later to call any public stuff.

public class CompBlazorChess_Logic:BlazorComponent
{
   protected override void OnInit()
   {
      if (ChessEngine1.compSettings.Curr_comp == null)
      {
         ChessEngine1.compSettings.Curr_comp = this;
      }
   }
}

Chessengine has compsettings property wich has all component references

public class CompSettings
{
        public CompBlazorChess_Logic Curr_comp = null;
        public CompChildBoard Curr_Comp_Board = null;
        public CompChildShape Curr_Comp_Shape = null;
        public CompChildStat Curr_Comp_Stat = null;
}

some method in chessengine

void abc()
{
     ....
    compSettings.Curr_comp.NotifyMadeMove(move);
     ....
}

call examples from different places

   compSettings.Curr_comp.Refresh();
   compSettings.Curr_comp.PlayerTime = TimeSpan.FromSeconds(Player_Total_Seconds).ToString(@"mm\:ss");
   compSettings.Curr_comp.NotifyGameOver();

I can save this instance reference in some static class too if I have only one component in my page (in chess case I have two components on page which play between each other, so I save component instances in chessengine instances not in static class):

public static class LocalData
{
        public static CompBlazorGameSnake_Logic Curr_comp = null;
        public static CompChildBoard Curr_Comp_Board = null;
        public static CompChildWalls Curr_Comp_Walls = null;
}
public class CompBlazorGameSnake_Logic : BlazorComponent
{
  protected override void OnInit()
  {
      LocalData.Curr_comp = this;
  }
}

Use example

public static void paint_Score()
{
   if (LocalData.Curr_comp != null)
   {
                int q = SnakeElements.Count - 1;
                if (q < 0)
                {
                    q = 0;
                }
                LocalData.Curr_comp.CurrPoint = "Point " + q.ToString();
                LocalData.Curr_comp.Refresh();
   }
}

Sometimes I link static property to component property because I use this value in many places and I want to access it as static value outside component rather than components property.

public int input_speed
{
           get
           {
               return LocalData.global_speed;
           }
           set
           {
               LocalData.global_speed = value;
               run();
           }
}

I hope it makes sense for you and is clear.

This tricks helps me to do what I want achieve.
Maybe they are not best practices but they worked for me.

Good look :)

@mkArtakMSFT mkArtakMSFT transferred this issue from dotnet/blazor Oct 27, 2019
@mkArtakMSFT mkArtakMSFT added the area-blazor Includes: Blazor, Razor Components label Oct 27, 2019
@ghost ghost locked as resolved and limited conversation to collaborators Dec 3, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

4 participants