I am building a dashboard which is having tiles that loads data on start up. The loading is shown with a loading animation and to prevent repeatable code, I want to manage this in a wrapper tile. This wrapper tile instance is passing itself to the child so the child can set the loading state to false when the data is retrieved. This is the code I have so far:
wrapper tile code
WrapperTile.razor
<MudItem xs="12" sm="6" md="4" lg="3" xl="2" xxl="1">
<MudPaper Height="250px" Elevation="3" Class="pa-4 mud-width-full">
#if (IsLoading)
{
<MudProgressCircular Color="Color.Primary" Size="Size.Large" Indeterminate="true" />
}
else
{
}
#if (ChildContent is not null)
{
#ChildContent(this)
}
</MudPaper>
</MudItem>
Side-note: The second if statement (checking the ChildContent) is currently outside the else statement for testing purposes but will be replaced as soon as the loading state switch process is working.
WrapperTile.razor.cs
public partial class WrapperTile
{
[Parameter]
public RenderFragment<WrapperTile>? ChildContent { get; set; }
public bool IsLoading { get; set; } = true;
}
test tile 1
TestTile1.razor
<WrapperTile>
Tile 1<br/>
<MudButton OnClick="Test">Test</MudButton>
</WrapperTile>
TestTile1.razor.cs
public partial class TestTile1
{
[Parameter]
public WrapperTile? Wrapper { get; set; }
private void Test()
{
ArgumentNullException.ThrowIfNull(Wrapper);
Wrapper.IsLoading = false;
}
}
test tile 2
Is the same as test tile 1, only the tile text is different (tile 2).
What I think that should happen is when calling the ChildContent(this) the wrapper tile is passed to the tile itself and the tile itself can change the IsLoading on the WrapperTile instance, changing the loading animation to the data that must be shown.
What works is the loading animation, it is shown as well as the tile text and the button. But as soon as the button is clicked, an exception is thrown by the ArgumentNullException.Throw() method, meaning that the Wrapper is null.
I cannot figure out why the Wrapper is null in this situation. Do I miss a constructor? Am I using the RenderFragment wrong this way? I also tried to use the CascadingValue component:
#if (ChildContent is not null)
{
<CascadingValue Value="this">
#ChildContent
</CascadingValue>
}
This also has the same issue, the Wrapper property is not set and causing the null reference exception.
What do I need to do to make this work the way I would like?
EDIT:
I have put both scenario's in a blazor fiddle.
RenderFragment with parameter: https://blazorfiddle.com/s/ozatfeb0
Using the CascadeValue/CascadingParameter: https://blazorfiddle.com/s/s6m4anbu
In both situations the Wrapper property is not set.
Related
I have a class MealsQueryInputs that I would like to use as a component parameter with two-way binding capabilities.
All of the demos and sample code I can find are using built-in primitive types and never a class. I can get the MS demos to work but I cannot get binding to a class to work. Is it even possible to do this?
My component FilterSortOptions.razor:
using WhatIsForDinner.Shared.Models
<MudCheckBox Checked="#QueryInputs.Favorite"
Color="Color.Inherit"
CheckedIcon="#Icons.Material.Filled.Favorite"
UncheckedIcon="#Icons.Material.Filled.FavoriteBorder"
T="bool"/>
<MudRating SelectedValue="#QueryInputs.Rating"/>
<MudButton OnClick="#(async () => await OnPropertyChanged())">Apply</MudButton>
#code {
[Parameter]
public MealsQueryInputs QueryInputs { get; set; }
[Parameter]
public EventCallback<MealsQueryInputs> QueryInputsChanged { get; set; }
private async Task OnPropertyChanged()
{
await QueryInputsChanged.InvokeAsync(QueryInputs);
}
}
As MrC said, you should avoid directly binding to the data being supplied as a parameter.
Here is a simple working sample (not MudBlazor) to show the concept
https://blazorrepl.telerik.com/QQEnQjaO54LY3MYK35
You bind to a local variable/property and try not to modify the incoming data directly.
MyComponent
<h1>MyComponent</h1>
<label for="choice">Choose</label>
<input id="choice" type="checkbox" #bind-value=localValue />
#code
{
bool localValue
{
get => Data.SomeChoice;
set {
if (value != localValue)
{
localData = Data with { SomeChoice = value };
InvokeAsync(ValueChanged);
}
}
}
ComplexObject localData;
[Parameter] public ComplexObject Data { get; set; }
[Parameter] public EventCallback<ComplexObject> DataChanged { get; set; }
Task ValueChanged() => DataChanged.InvokeAsync(localData);
}
ComplexObject
public record ComplexObject(bool SomeChoice, string SomeText);
Main
#code
{
ComplexObject data = new(false,"");
}
<MyComponent #bind-Data=data />
You have chosen #data.SomeChoice
Here is how you can bind class objects to a custom razor component
This is FilterSortOptions component
<div>
<label>Rating:</label>
<input type="text" value=#QueryInputs.Rating #oninput=#(val=> {
QueryInputs.Rating=val.Value.ToString();
QueryInputsChanged.InvokeAsync(QueryInputs);
}) />
</div>
<div>
<label>Favourite:</label>
<input type="checkbox" value=#QueryInputs.Rating #onchange=#(val=> {
QueryInputs.Favourite=(bool)val.Value;
QueryInputsChanged.InvokeAsync(QueryInputs);
}) />
</div>
#code {
[Parameter]
public MealsQueryInputs QueryInputs { get; set; }
[Parameter]
public EventCallback<MealsQueryInputs> QueryInputsChanged { get; set; }
}
This is the model to bind, for simplicity Rating is is string type
public class MealsQueryInputs
{
public bool Favourite { get; set; } = false;
public string Rating { get; set; } = "0";
}
Here is the razor page
<h3>Rating: #QueryInputs.Rating</h3>
<h3>Favourite: #QueryInputs.Favourite</h3>
<FilterSortOptions #bind-QueryInputs=#QueryInputs></FilterSortOptions>
#code {
public MealsQueryInputs QueryInputs = new();
}
Updated Answer
Firstly, if your using an object then you are passing around references to the same object. So when you update the object in the sub-component, you're updating the same object the parent is using. You don't need to pass the object back in the callback unless you create a noew copy of it.
Secondly, your not binding the mud controls to the object.
Let's look at your code:
<MudCheckBox Checked="#QueryInputs.Favorite"
Color="Color.Inherit"
CheckedIcon="#Icons.Material.Filled.Favorite"
UncheckedIcon="#Icons.Material.Filled.FavoriteBorder"
T="bool"/>
Checked="#QueryInputs.Favorite" doesn't bind the control to the field. It just sets the initial value.
I think (I don't use Mudblazor and it's a little different from standard Blazor Form Controls) you need to do this:
<MudCheckBox #bind-Checked="#QueryInputs.Favorite"></MudCheckBox>
The same is true for MudRating.
<MudRating #bind-SelectedValue="#QueryInputs.Rating" />
Then the button:
<MudButton OnClick="#(async () => await OnPropertyChanged())">Apply</MudButton>
can be simplified to this. You're wrapping an async method within an async method.
<MudButton OnClick="OnPropertyChanged">Apply</MudButton>
// or
<MudButton OnClick="() => OnPropertyChanged()">Apply</MudButton>
Original Answer
There are a couple of issues here:
QueryInputs is a Parameter and therefore should never be modified by the code within the component. You end up with a mismatch between what the Renderer thinks the value is and what it actually is.
When the parent component renders it will always cause a re-render of any component that is passed a class as a parameter. The Renderer has no way of telling if a class has been modified, so it applies the heavy handed solution - call SetParametersAsync on the component.
A solution is to use a view service to hold the data and events to notify changes. One version of the truth! Search "Blazor Notification Pattern" for examples of how to implement this. I'll post some code if you can't find what you want.
I have a blazor application where I have the main window and other files which are the child windows.
I'm looking for a way for the main window button to open the child window, but even though the EnableConf value is true, the window won't open, I tried using StateHasChanged() and it still doesn't work.
CheckState.cs
public class CheckState {
public bool EnableConf { get; set; } = false;
}
MainWindow.cs in MainWindow()
Collect.AddScoped<CheckState>();
Conf.razor
#inject AppBlaz.CheckState CheckState
if(CheckState.EnableConf) {
<style>
...
</style>
...
<button #onclick="()=> CloseConf()">
...
}
#code {
private void CloseConf() {
CheckState.EnableConf = false;
}
}
main.razor
#inject AppBlaz.CheckState CheckState
...
<button #onclick="()=> OpenConf()">
...
#code {
private void OpenConf() {
CheckState.EnableConf = true;
}
}
The child window close button works perfectly, the only problem is that the main window cannot open the child window.
Does anyone know how I can make this work correctly
Presuming that somewhere in the main.razor you actually have placed the child window component (Conf.razor) to be hidden or displayed and there is no java script / third party lib that popups this window:
Add
[Parameter]
public bool CheckState {get; set;}
[Parameter]
public EventCallback<bool> CheckStateChanged { get; set; }
to your Conf.razor. Use this property as replacement for CheckState.EnableConf - if needed, then in OnAfterRender() override assign value to new property from currently used service (CheckState).
if(CheckState.EnableConf) becames if(CheckState).
In main.razor:
Repeat the above, but ommit [parameter] attribute for CheckState and drop whole CheckStateChanged including the [Parameter].
In place where the Conf.razor is initialized / put into actual ui code add
<Conf #Bind-CheckState=CheckState /> - now you will pass this newly created property down the line to child window.
In OpenConf() method change the newly created CheckState parameter instead of / in addition to CheckState.EnableConf.
CloseConf now should re
This should trigger the UI update. In case of emergency, add StateHasChanged() at the end of OpenConf() method.
Again, this should work (more or less, I typed in from memory) for standard blazor components.
I have a similar component and I whish to save the instance of the InnerComponent the first time it is rendered and render the same instance every time without reinstanceiating it.
#if(isVisible)
{
<InnerComponent #ref="#_InnerComponent" #key="#("InnerComponentKey")">
#ChildContent
</InnerComponent>
}
#code{
[Parameter] public InnerComponent _InnerComponent { get; set; }
private bool IsVisible { get; set; }
}
When the inner component is visible the user can manipulate its state.
But if IsVisible gets set to false and then true again, the inner component gets rerendered overriding _InnerComponent and thus we lose track of the changes that the user made to that InnerComponent instance.
Adding #key does not seem to help preserve the instance either. It just gets rerendered and overwritten :/ I am sure, that I am feeding it the same key both times it gets rendered, but I don't know how to inspect the keys that it gets compared to.
If it is possible to render a component instance I could do something like the following, but I can't seem to find a way to do that.
#if(isVisible)
{
#if(_InnerComponent == null)
{
<InnerComponent #ref="#_InnerComponent" #key="#("InnerComponentKey")">
#ChildContent
</InnerComponent>
}
else
{
#_InnerComponent.Render()
}
}
I am taking criticism on my question since I haven't asked many :)
Thanks in advance!
Simplified example:
Let's say we have the following component that I am going to call `CounterContainer` , where `<Counter>` is the counter component from the default Blazor project template.
#if(CounterIsVisible)
{
<Counter #ref="#_Counter" #key="#("CounterKey")" />
}
<button #onclick="() => CounterIsVisible = !CounterIsVisible">
Show/Hide counter
</button>
#code{
[Parameter] public Counter _Counter { get; set; }
private bool CounterIsVisible { get; set; } = true;
}
I want to save the _Counter instance, so I can see the correct _Counter.currentCount that I counted to. I could save it using a method from this article, but I find them all unpractical, because
the component I am building has much more data than just a single variable
I need the data only as long as the CounterContainer exists and only for visualisation
It is just too complicated for my use case
I already have the Counter reference stored. I just want to view it instead of reinstanceiating it and overwriting the whole thing.
Hope that made it a bit clearer (:
I'm surprised it compiles, since you've misspelled your component class:
[Parameter] public **InnerCopmonent** _InnerComponent { get; set; }
Why do you have an #ref to your InnerComponent at all? What does InnerComponent do, and why do you want to reference it? Can you share that component's code?
best option: separate View and Data. The state you want to preserve should not be kept inside the component but in a separate (ViewModel) object.
short solution: always render the component. Use #isVisible to turn a css-class on/off to hide it.
The way I like to do this kind of thing is to create a carrying class which I instantiate in the parent and pass to the child as a Parameter. Since a class is passed by reference, I don't need to worry about events or anything like that. (I just made up a class for demo to show you can have whatever you want in it)
CounterData.cs
public class CounterData
{
public int counter { get; set; }
public string Title { get; set; } = "Counter";
public List<int> CounterHistory { get; set; } = new List<int>();
}
CounterContainer.razor
#page "/counter"
<h3>Counter Container</h3>
<button #onclick="()=> ShowCounter=!ShowCounter">Toggle</button>
#if (ShowCounter)
{
<Counter CarryingInstance="PersistentData" />
}
#code {
CounterData PersistentData = new CounterData();
bool ShowCounter = false;
}
Counter.razor
<h3>#CarryingInstance.Title</h3>
<input #bind="CarryingInstance.Title" /><br />
<button #onclick="()=> Increment(1)">Add</button>
<button #onclick="()=>Increment(-1)">Sub</button>
#CarryingInstance.counter <br />
History:
#foreach (var item in CarryingInstance.CounterHistory)
{
<span> #item</span>
}
#code {
[Parameter]
public CounterData CarryingInstance { get; set; }
void Increment (int amount)
{
CarryingInstance.counter += amount;
CarryingInstance.CounterHistory.Add(CarryingInstance.counter);
}
}
I've seen the option of hiding the component with CSS and the unexpected problems it can cause because components then aren't going through normal life cycle events when they are legitimately rerendered/reinstantiated. So I'm busy updating several components to fix that exact thing. My advice would be to avoid doing that if possible.
Another way, similar to BennyBoys carrying instance method, is you could probably do it is by utilizing dependency injection and registering a type as a scoped or singleton service and having it injected into the component.
You can read here on Chris Sainty's blog about how the different registrations work in blazor and their behaviour and if it'll fit your needs.
https://chrissainty.com/service-lifetimes-in-blazor/
I would double check via official docs that these still behave the same way though just in case but it might be a nice way to do it. I think it depends on what your actual use case is whether it's a good idea to do it like this, but it's another option :)
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?
<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.
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
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.
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.
I have a view in my project with 2 nested views/components (the main view contains a tree view and the tree view contains individual 'node' views for each object in the tree).
I'm using event callbacks to get the clicked object's ID property (an int) back up to the top-tier view to let the main view know which level has been clicked, but the problem comes up in the last view. When I debug it, the onclick method seems to change the LevelID to 0, regardless of what it is before the method executes.
<DxTreeViewNode Text="#Level.LevelName" #onclick="#(async () => await SelectedLevelID.InvokeAsync(LevelID))">
#code {
private LevelInfo Level { get; set; }
[Parameter]
public int LevelID { get; set; }
protected override async Task OnInitializedAsync()
{
Level = await nodeViewModel.GetLevelInfoFromIDAsync(LevelID);
}
[Parameter]
public EventCallback<int> SelectedLevelID { get; set; }
}
If I hard code the value (e.g. InvokeAsync(1)), then it works as expected and clicking any level shows the data from level 1, otherwise it sets the views LevelID property to 0. I don't know if this is a bug or if I'm not using the callback properly. Thanks for any help :)
The problem was indeed that the click event was propagating so clicking a nested item triggered its own event, followed by the event of its parent, then its grandparent... There is no current way in Blazor to stop back propogation of events, but it is planned for a future release. I might get around it with a method which checks a bool is true and changes it to false so that only a single event callback gets triggered successfully, then reset that bool in the parent view, but that's a lot of faff.