One-way [Parameter] binding programatically - c#

I want to set defaults for parameters on a third-party component. Say I have this:
myBasePage.cs:
public class MyBasePage : ComponentBase
{
public IEnumerable MyData { get; set; }
}
myPage.razor:
#inherits MyBasePage
<ThirdPartyComponent Data="#MyData" />
Since Data on ThirdPartyComponent is a [Parameter] with a DataHasChanged() virtual method, by rendering the blazor component like that, I'll get one-way binding, and if I change MyData on my page, programmatically, the component will update. This will work fine.
Now, say I can't modify ThirdPartyComponent, but I want to make some defaults in it based on my base page... like so:
myPage.razor:
#inherits MyBasePage
<MyDerivedComponent PageComponent="#this" />
myDerivedComponent.cs:
public class MyDerivedComponent : ThirdPartyComponent
{
[Parameter] public MyBasePage PageComponent { get; set; }
public override void OnInitialized()
{
/* Set other parameter defaults */
this.OtherParameter = 10;
/* Bind to Data, as if I was passing it as a parameter in the Razor template */
this.Data = PageComponent.MyData;
}
}
This line:
this.Data = PageComponent.MyData;
Doesn't create a binding at all (and if I modify MyData, the blazor component doesn't get updated). Is there any way to programmatically create it?
Note: the real ThirdPartyComponent includes not only tons of parameters but also templates, etc. For many reasons, I'd like MyDerivedComponent to be of a derived type, and not a "parent component" with a child of ThirdPartyComponent, if that's possible at all).

This should work:
public class MyDerivedComponent : ThirdPartyComponent
{
[Parameter] public MyBasePage PageComponent { get; set; }
public override void OnInitialized()
{
/* Set other parameter defaults */
this.OtherParameter = 10;
//this.Data = PageComponent.MyData;
}
}
protected override void OnParametersSet()
{
Data = Data ?? PageComponent?.MyData ; // determine priority
base.OnParametersSet();
}
OnParametersSet() notifies the Component that it has new Parameters. The most-derived class can intervene here.
But there is no easy solution for wwhen this.MyData changes, that's out of sight for Blazor.

Is there any way to programmatically create it? Of course, but that doesn't mean that you should.
Parameters should not be set or manipulated within the component code. Parameters are set externally whenever SetParametersAsync is called by the Renderer. If you change them internally you create two versions of the truth.

Related

Blazor: intercept an EventCallback to add some code

In my Blazor WebAssembly application, I use a lot the TelerikGrid component, provided by Telerik Kendo, but my question could be the same with another component.
I need to execute some code at the end of the execution of the handler of the "OnRead" EventCallback of TelerikGrid, as bellow:
protected async Task OnReadHandler(GridReadEventArgs args)
{
args.Data = _myClient.GetData(); //some code
// This line is the one I need to repeat accros all handlers of OnRead in my application
await JSRuntime.InvokeVoidAsync("attachAllGridCells");
}
I do not know if Blazor can achieve my request. In addition, the component TelerikGrid cannot be modified, as it is from a third-party. I've tried something, with no good result:
Create a component which extend TelerikGrid, and trying to somehow override OnRead. But as it is an EventCallback, and not a method, I cannot do that easily:
public partial class CustomTelerikGrid<T> : TelerikGrid<T>
{
[Parameter]
public new EventCallback<GridReadEventArgs> OnRead { get; set; }
async Task TestAsync()
{
await this.OnRead.InvokeAsync(); // access to my "new" eventcallback
await base.OnRead.InvokeAsync(); // access to the "original" eventcallback
}
}
Is there way I can achieve that? I believe not really with my knowledge of Blazor, but maybe someone can have an idea, thanks in advance for any help.
We have a grid component in our project that extends TelerikGrid. We made class TelerikGridSettings with parameters from base Telerik Grid which we use in our grids across our project like that:
public partial class TelerikGridSettings<TItem> : BaseComponent
{
[Parameter]
public IEnumerable<TItem> Data { get; set; }
[Parameter]
public decimal RowHeight { get; set; }
[Parameter]
public RenderFragment GridColumns { get; set; }
[Parameter]
public EventCallback<GridReadEventArgs> OnRead { get; set; }
/// etc
}
Then in your custom GridComponent you use TelerikGrid and fill parameters like that, and also insert your custom OnReadHandler:
<TelerikGrid TItem="TItem"
Data=#Data
RowHeight=#RowHeight
GridColumns=#GridColumns
OnRead=#OnReadHandler
// etc />
In your OnReadHandler you will invoke TelerikGrid common OnRead event callback and after that use your JS method:
private async ValueTask OnReadHandler()
{
await OnRead.InvokeAsync();
// your js method
}
Then you can use your GridComponent exactly as you use TelerikGrid right now.
Hope this helps. Also this approach will give you more flexibility in customizing your grid.

how to use generic components with multiple types in blazor

I'm making a generic grid component in blazor c# to handle CRUD operations and master detail relations.
now I have two generic components
public class GridM<TParentModel> where TParentModel : new() { // My code }
and I have
public class GridD<TParentModel,TChildModel> : GridM<TChildModel>where TParentModel : new()
{
// here I'm overriding some methods from the parent class to handle relational stuff
}
so for plain tables I'm using GridM to display my data and when I have 1 to many relations with the master table I'm using GridD to display the relational data.
so far so good, I do not have any issues with the above approach. Then after putting some thoughts on this approach I've decided to merge GridM and GridD into one class like this:
public class MyGrid<TModel> {}
public class MyGrid<TModel, TChild> : MyGrid<TChild>{ // and override methods here }
this in theory works fine and assuming these implementations were not components
I would be able to initialize them like
var a = new MyGrid<Users>();
var b = new MyGrid<Users,Orders>();
so when I try to use this component like
< MyGrid TModel="UserModel"> </MyGrid >
I get the following error:
The component 'MyGrid' is missing required type arguments. Specify the missing types using the attributes: 'TChildModel'.
because blazor does not now whether I'm using MyGrid<TModel> or MyGrid<TModel,TChildModel>.
my question is how can I make blazor understand this?
You don't have one class, you have two. The Razor compiler doesn't know which class to map the Tag to, so it throws the error:
Multiple components use the tag Fred. Components: SO73613449.Data.Fred<TModel>, SO73613449.Data.Fred<TModel, TType>
You can't use them directly in Razor markup, but you can in RenderTreeBuilder code in a RenderFragment.
Here's some code to demo how to do it:
First my classes:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace SO73613449.Data;
public class Fred<TModel> : ComponentBase
{
public TModel? Model { get; set; }
protected string name = "Fred (TModel)";
protected string cssClass = "alert alert-primary";
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", cssClass);
builder.AddMarkupContent(2, $"Hello from {name} <br /> {this.GetType().FullName}");
builder.CloseElement();
}
}
public class Fred<TModel, TType> : Fred<TModel>
{
public TType? Type { get; set; }
public Fred()
{
name = "Fred(TModel, TType)";
cssClass = "alert alert-dark";
}
}
And then a demo page:
#page "/"
#using Microsoft.AspNetCore.Components.Rendering;
#using SO73613449.Data;
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
#TestFirst
#TestSecond
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
#code {
private RenderFragment TestFirst => __builder =>
{
__builder.OpenComponent<Fred<int>>(0);
__builder.CloseComponent();
};
private RenderFragment TestSecond => __builder =>
{
__builder.OpenComponent<Fred<int, string>>(0);
__builder.CloseComponent();
};
}
And here's what it looks like:
The alternative is to just give them two names!

Two-Way Component Parameter Binding on a class in Blazor?

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.

How to swap data-bound page in WPF MVVM application?

I have a window that displays templates in a tree, these can be selected which updates a ListView with available fields in the template. All related operations until here are managed by the TemplateViewModel declared at windows level as:
<Window.DataContext>
<vm:TemplateViewModel/>
</Window.DataContext>
extract of the class:
public class TemplateViewModel : ViewModelBase,INotifyPropertyChanged
{
public FieldTypeViewModel FieldTypeView { get; }
public TemplateViewModel()
{
// Create additional view
FieldTypeView = new FieldTypeViewModel(this);
...
}
Each template field has an identifier and type which are still managed by this view (all working up to here).
Now depending on the type of the field a different page is to be displayed in a reserved window part (Frame). Also the type view model is a separate view model class FieldTypeView .
The FieldType object is created in the constructor of the TemplateViewModel and saved in the FieldTypeView property as it needs to be linked to this model for updating as field gets selected.
Both views used to implement the INotifyPropertyChanged interface but since the FieldTypeView is created by the view and not by the window defintion the notification event is not set, so I currently call the parent (TemplateViewModel) event for notification.
So I have a frame defined as:
<Frame DataContext="{Binding FieldTypeView}" Grid.Row="1" Content="{Binding CurrentFieldTypeSetupPage}"/>
public class FieldTypeViewModel : ViewModelBase
{
private TemplateViewModel _templateViewModel;
private TTemplateFieldType? _FieldType;
public TTemplateFieldType? FieldType
{
get { return _FieldType; }
set { _FieldType = value;
UpdateFieldType();
NotifyPropertyChanged("FieldType"); }
}
private Page? _CurrentFieldTypeSetupPage;
public Page? CurrentFieldTypeSetupPage
{
get { return _CurrentFieldTypeSetupPage; }
set { _CurrentFieldTypeSetupPage = value; NotifyPropertyChanged("CurrentFieldTypeSetupPage"); }
}
// Define property per type for easy data context access
public TTFTText? tfText { get; set; }
public TTFTDate? tfDate { get; set; }
//------------------------------------------------------------------------------
private void UpdateFieldType()
{
// Set the appropriate field type, and "null" the others
tfText = _FieldType as TTFTText;
tfDate = _FieldType as TTFTDate;
if (_FieldType != null)
{
CurrentFieldTypeSetupPage = _FieldType.GetSetupPage();
}
else
{
CurrentFieldTypeSetupPage = null;
}
}
//------------------------------------------------------------------------------
public void NotifyPropertyChanged(string prop)
{
_templateViewModel.NotifyPropertyChanged(prop);
}
//------------------------------------------------------------------------------
public FieldTypeViewModel(TemplateViewModel templateVM)
{
_templateViewModel = templateVM;
}
}
Every time the field selection changes the TemplateViewModel does set the FieldTypeView which gets the correct window for the current type and sets its CurrentFieldTypeSetupPage, which finally notifies the change via NotifyPropertyChanged("CurrentFieldTypeSetupPage"); which actually calls the TemplateViewModel's NotifyPropertyChanged method calling the event handler to notify the change.
Note that notification in the TemplateViewModel works for all its other fields, but the type page is never shown.
So the question is what I am doing wrong or what is the correct way to implement dynamic page changing in MVVM. My guess is that INotifyPropertyChange is not the correct way to go ?

WebAPI deserialization of a protected property is null

My solution has a WebAPI project (.net core 3.1, Microsoft.AspNetCore.Mvc) and a (.Net Standard 2.1) class library that defines the data structures.
My Controller takes a post with a single parameter that deserializes mostly correctly
public class apiRequest
{
public RequestData TheData { get; set; }
public Options Options { get; set; }
public apiRequest() { }
}
The RequestData and child objects are defined i a .Net Standard 2.1 class library and added via a nuget package
public class RequestData : IRequestData
{
public int Datum{ get; set; }
...
public List<ComplexItem> ComplexItems { get; set; }
...
}
public class ComplexItem: ItemBase, IComplexItem
{
public ComplexItem() : base() { }
public ComplexItem(Pricing defaultPricing) : base(defaultPricing) { }
[JsonConstructor]
public ComplexItem(Pricing defaultPricing, Pricing selectedPricing) : base(defaultPricing, selectedPricing) { }
}
The problem I am running into is with the defaultPricing is always null when it gets to the controller
public class ItemBase : IItemBase
{
public ItemBase () { }
public ItemBase (Pricing defaultPricing)
{
DefaultPricing = defaultPricing;
}
[JsonConstructor]
public ItemBase (Pricing defaultPricing, Pricing selectedPricing)
{
DefaultPricing = defaultPricing;
SelectedPricing = selectedPricing;
}
#region Pricing
[JsonProperty]
protected Pricing DefaultPricing { get; set; }
public Pricing SelectedPricing { get; set; }
[JsonIgnore]
protected Pricing CurrentPricing
{
get { return SelectedPricing ?? DefaultPricing; }
set { SelectedPricing = value; }
}
[JsonIgnore]
public decimal Cost { get => CurrentPricing?.Cost ?? 0; }
[JsonIgnore]
public decimal Price { get => CurrentPricing?.Price ?? 0; }
#endregion
}
I've tried using [DataContract] and [DataMember] attributes, JsonObject, JsonConstructor, JsonProperty attributes and [Serializable] attribute. (Is there a current best practice on what to use?)
If I read the Json from a file and use Newtonsoft.Json.JsonConvert.DeserializeObject it deserializes correctly with the Json attributes added, but still null in the controller.
It also deserializes in the API properly if I make it public, so it doesn't seem like a problem in the Pricing class itself
After posting I found this Question about making Newtonsoft the default and using MikeBeaton's accepted solution there with Microsoft.AspNetCore.Mvc.NewtonsoftJson package worked so I'll put this as one potential answer for anyone else with this issue. Would still like to know if there is a more correct solution available.
System.Text.Json Serializes Public Properties
As the documentation implies (emphasis mine):
By default, all (read: only) public properties are serialized. You can specify properties to exclude.
I would guess that this was the design chosen because serializing an object is allowing that object to cross barriers of scope and the public scope is the only one that can reliably be assumed.
If you think about it, it makes sense. Lets say, you define a protected property and serialize the object. Then a client picks it up and deserializates that text representation into a public property. What you have designed to be an implementation detail of/to derived types is now accessible outside the scope defined by the modifier.
Apart from simply pointing you to your own answer where Newtonsoft allows this protected property to be serialized, I would suggest you look more intently at your design and why those properties are protected in the first place. It makes sense within the context of your API implementation, but the client can't (shouldn't) be assumed to follow your same inheritance structure (or support inheritance at all). It seems like you might want to define a true DTO to act as the "shape" of your API response and find the right place to transition from your internal types using protected scope to control access and the DTO that can cross the border of the API.

Categories

Resources