mz1378
mz1378

Reputation: 2582

How in Blazor Set component properties through a Reference that obtained by @Ref

I have a component that I set a reference of it in a page variable:

<BlazorWebFormsComponents.Button OnClick="@((args) => btnForms_Clicked(formsButton, args))" @ref="formsButton" Text="Forms Button" CssClass="btn btn-primary">
    
</BlazorWebFormsComponents.Button>

In the event handler I Set a button property (Text):

Button formsButton;

public void btnForms_Clicked(object sender, MouseEventArgs e)
{                        
    if (sender is Button)
        (sender as Button).Text = "Good Bye";
}

For most of the Button properties this code is not working, For BackColor works but for Text not. Also blazor makes the assignment line, a green Underlined and says "Component parameter "zzz" should not be set outside of its component", So why Blazor provides a @Ref while most of the referenced properties can not be set? or is there a way to make this work?

Upvotes: 3

Views: 16236

Answers (4)

Gary O. Stenstrom
Gary O. Stenstrom

Reputation: 2294

What I do for Parameters to make them "settable" in the code block/behind is to designate a "setter" method for them. The following code, while it should work, is untested but illustrates the concept.

MyComponent.razor - A simple component that wraps a button and exposes it's OnClick event to consumer components/"pages"

<Button OnClick="btn_OnClick"></Button>

@code{
        
    [Parameter]
    public EventCallback<MouseEventArgs?> MyComponent_OnClick { get; set; }


    // Handles the Click event from the button and raises the EventCallback assigned to the 
    // component via the MyComponent_OnClick Parameter.
    //
    private async Task btn_OnClick(MouseEventArgs args)
    {
        if(this.MyComponent_OnClick.HasDelegate)
        {
            await this.MyComponent_OnClick.InvokeAsync(args);
        }
    }       

    // Assigns the eventCallback argument to the MyComponent_OnClick Parameter 
    // without violating compiler rules. 
    //
    public void AddEventListener_MyComponentOnClick(EventCallback<MouseEventArgs?> eventCallback)
    {
        this.MyComponent_OnClick = eventCallback;
    }

}

Page.razor - Consumer component/"page". Notice that the "Click" event handler is not assigned declaratively in the tag but is instead assigned when the component/"page" is first initialized.

@page "/Test"

<MyComponent @ref="myComponent"></MyComponent>

@code {

    // Reference to the MyComponent instance
    //
    protected MyComponent? myComponent;

    // Runs when the "page" is first loaded.
    // 
    protected override Task OnInitializedAsync()
    {
        // Use the public "AddEventListener" method to programmatically assign an 
        // EventCallback to the OnClick event of the MyComponent.
        //
        mtComponent?.AddEventListener_MyComponentOnClick(EventCallback.Factory.Create<int?>(this, this.ComponentOnClick));

        return base.OnInitializedAsync();
    }

    protected void ComponentOnClick(MouseEventArgs args)
    {
        // ... Handle the event
    }       

}

This way you could potentially assign an appropriate event handler based on some condition. Or when passing a component reference to descendent components via the CascadingValue tag, it allows you to apply the event handler when the declarative syntax is not available.

For example ...

MainLayout.razor

<MyComponent @ref="myComponent"></MyComponent>

<CascadingValue Value=@myComponent Name="MyComponent">
    @body
</CascadingValue>

@code {

        public MyComponent? myComponent;
}

I'm not a technical writer but I hope that someone was able to find this useful.

Upvotes: 0

enet
enet

Reputation: 45586

<Button OnClick="@((args) => btnForms_Clicked(formsButton, args))" @ref="formsButton" Text="Forms Button" CssClass="btn btn-primary">

Your Button component should be defined as follows:

@code
{
    [Parameter]
    public EventCallback<MouseEventArgs> OnClick {get; set;} 
    [Parameter]
    public string Text {get; set;} 

}

The above code define two parameter properties that should be assigned from the Parent component. The parent component is the component in which the Button component is instantiated. Note that you should set the above properties from the parent component as attribute properties... you must not set them outside of the component instantiation. Right now it's a warning, but Steve Anderson has already sad that it is going to be a compiler error soon. This is how you instantiate your component in the parent component:

Parent.razor

<Button OnClick="@((args) => btnForms_Clicked(args))" @ref="formsButton" 
      Text="_text" CssClass="btn btn-primary">
    
</Button>

@code
{
private Button formsButton;

// Define a local variable wich is bound to the Text parameter
private string _text = "Click me now...";

public void btnForms_Clicked( MouseEventArgs e)
{                        
    _text = "You're a good clicker.";
}
}

Note: When you click on the Button component a click event should be raised in the Button component, and the button component should propagate this event to the parent component; that is to execute the btnForms_Clicked method on the parent component, Here's how you do that:

Button.razor

<div @onclick="InvokeOnClick">@Text</div>

@code
    {
        [Parameter]
        public EventCallback<MouseEventArgs> OnClick {get; set;} 
        [Parameter]
        public string Text {get; set;} 

        private async Task InvokeOnClick ()
        {
             await OnClick.InvokeAsync(); 
        }
    
    } 

Note that in order to demonstrate how to raise an event in the Button component, and propagate it to the parent component, I'm using a div element, but you can use a button element, etc.

@onclick is a compiler directive instructing to create an EventCallback 'delegate' whose value is the event handler InvokeOnClick. Now, whenever you click on the div element, the click event is raised, and the event handler InvokeOnClick is called... from this event we execute the EventCallback 'delegate' OnClick; in other words, we call the btnForms_Clicked method defined in the parent component.

So what the @ref directive is good for? You may use the @ref directive to get a reference to a component that contain a method you want to call from its parent component: Suppose you define a child component that serves as a dialog widget, and this component define a Show method, that when is called, display the dialog widget. This is fine and legitimate, but never try to change or set parameter properties outside of the component instantiation.

Upvotes: 4

Mister Magoo
Mister Magoo

Reputation: 8954

The warning is there because setting [Parameter] properties from code can cause the render tree to get out of sync, and cause double rendering of the component.

If you need to set something from code, you can expose a public method e.g. SetText on the component class, which does that for you.

Internally, the component [Parameter] should reference a local variable.

string _text;
[Parameter] 
public string Text { get => _text; set => SetText(value);}
public void SetText(string value)
{
  _text = value;
}

I am not promoting this approach, I prefer to use the approach in @Pidon's answer. Additionally, you could consider - maybe you have too many parameters and should consider an Options parameter to consolidate

Upvotes: 1

Pidon
Pidon

Reputation: 275

I'm not an export on Blazor, but ran into this as well. You can do this by binding to a property on the page.

<BlazorWebFormsComponents.Button OnClick="@((args) => btnForms_Clicked(formsButton, args))" @ref="formsButton" Text="@ButtonText" CssClass="btn btn-primary">

</BlazorWebFormsComponents.Button>

@code{
    private string ButtonText { get; set; } = "Forms button";

    public void btnForms_Clicked(object sender, MouseEventArgs e)
    {
        ButtonText = "Good bye";
    }
}

I have never used the @Ref and don't yet know how or when to use it.

Upvotes: 0

Related Questions