Blazor Custom Bind - Get Property of model that changed - c#

I receive data from a SQL Server and load it into a model:
namespace BlazorServer.Models
{
public class Animal
{
public int height { get; set; }
public string name { get; set; }
public string origin { get; set; }
}
}
In a component I use the model to display data. The user is able to edit the data. Here is a sample:
<input #onchange="args => ValueChanged(args)" value="#animal.name" class="form-control form-control-sm border rounded" />
How do I get which property of the model has changed? I tried the following, but I only get the new value:
private void ValueChanged(ChangeEventArgs args)
{
var newValue = args.Value;
}
I want to update the model in the component (which equals the binding of blazor) AND also send the data to the SQL server right away.

Blazor comes with EditForm which manages an EditContext, and a set of Input controls - InputText in your case - that interface with EditContext. You can access the EditContext, register an event handler on OnFieldChanged and get change events. You get passed a FieldIdentifier that you can use to identify which field has been changed.
See - MS Documentation
Here's a simple razor page that demos using EditContext and OnFieldChanged
#page "/Test"
#implements IDisposable
<EditForm EditContext="this.editContext" class="m-3">
Animal: <InputText #bind-Value="this.model.name"></InputText><br />
Origin: <InputText #bind-Value="this.model.origin"></InputText><br />
</EditForm>
<div class="m-3">FieldChanged:<i>#this.FieldChanged</i> </div>
#code {
public class Animal
{
public int height { get; set; }
public string name { get; set; }
public string origin { get; set; }
}
private Animal model = new Animal() { height = 2, name = "giraffe", origin = "Africa" };
private EditContext editContext;
private string FieldChanged = "none";
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(model);
this.editContext.OnFieldChanged += this.OnFieldChanged;
return base.OnInitializedAsync();
}
private void OnFieldChanged(object sender, FieldChangedEventArgs e)
{
var x = e.FieldIdentifier;
this.FieldChanged = e.FieldIdentifier.FieldName;
}
// Need to Implement IDisosable to unhook event handler
public void Dispose ()
{
this.editContext.OnFieldChanged -= OnFieldChanged;
}
}

Related

Blazor value pass to the parent from dynamically rendered child

I'm using Radzen data grid on my .NET core 3.1 blazor app. On the grid page I have RadzenDataGridColumn component and within that I have FilterTemplate component which is rendered dynamically. My problem is I want to bind FilterValue to the FilterTemplate component and the changes were made to the FilterValue should pass back to the parent level.
<RadzenDataGridColumn Property="#colDef.Property" FilterValue="#colDef.FilterValue" >
<FilterTemplate>
<CascadingValue Value="#colDef.FilterValue">
#colDef.FilterTemplate //this contains a RenderFragment
</CascadingValue>
</FilterTemplate>
</RadzenDataGridColumn>
colDef.FilterTemplate can contain code snippet like bellow.
<RadzenDropDown TValue="string" #bind-Value="FilterValue" Data="#DrpData" TextProperty="#DrpTextProperty" ValueProperty="#DrpValueProperty" ></RadzenDropDown>
#code {
[CascadingParameter]
public string FilterValue { get; set; }
[Parameter]
public string DrpTextProperty { get; set; }
[Parameter]
public string DrpValueProperty { get; set; }
[Parameter]
public IEnumerable<DrpDataType> DrpData { get; set; }
}
Use event callbacks, like in this example. I used the default Blazor wasm template and the existing Counter component to show how it works:
In child component (Counter.razor)
[Parameter]
public EventCallback<string> OnFilterValueChanged { get; set; }
private async Task IncrementCount()
{
currentCount++;
string newFilter = currentCount.ToString();
await OnFilterValueChanged.InvokeAsync(newFilter);
}
The parent (Index.razor):
<Counter OnFilterValueChanged="FilterValueHandler" />
<h2>#FilterValue</h2>
#code {
public string FilterValue { get; set; } = "Initial Value";
private void FilterValueHandler(string filtervalue)
{
FilterValue = filtervalue;
}
}
Now, when you click the button in child component, the variable value in parent component will get updated.

Event callback with blazor

I am learning about EventCallBack with blazor, but i wanted to know
Is there a way for my return method to return a second argument on the parent side. I would like to know the rowID that it came from
Parent Code
#foreach (var rw in pg.rows)
{
#if (rw.rowDef == 1)
{
#if (rw.widgetsSection1.Count == 0)
{
<AddWidgetToColumnComponent rowID="rw.rowID" onAddingWidget="AddWidgetToRow"></AddWidgetToColumnComponent>
}
}
}
}
#code{
...
private void AddWidgetToRow(GenericWidgets evnt)
{
//bool confirmed = await JsRuntime.InvokeAsync<bool>("confirm", "Are you sure?");
var row = pg.rows.Where(x => x.rowID == 3).FirstOrDefault();
//row.addWidgetSection1(widgets);
}
}
Child Component AddWidgetToColumnComponent
<button class="btn btn-info" #onclick="#addComponent">Ajouter</button>
#code {
[Parameter]
public int rowID { get; set; }
[Parameter]
public EventCallback<GenericWidgets> onAddingWidget { get; set; }
public async void addComponent()
{
GenericWidgets widget = new GenericWidgets();
await onAddingWidget.InvokeAsync(widget);
}
I am not sure how to accomplish this, if possible (debugger give me error). The only other way would be to change my generic class to add the rowID attribute in it. But is there an other way?
Change the lines ( what i would like it to be)
<AddWidgetToColumnComponent rowID="rw.rowID" onAddingWidget="AddWidgetToRow(rw.rowID)"></AddWidgetToColumnComponent>
and this line
private void AddWidgetToRow(GenericWidgets evnt, int rowID)
If the question is about how to get an EventCallback to fire on the parent with multiple parameters, I usually achieve this by creating a class to encapsulate everything that needs to get passed.
So you might end up with something like:
[Parameter] public EventCallback<AddWidgetArgs> onAddingWidget { get; set; }
And AddWidgetArgs is a class, like:
public class AddWidgetArgs
{
public GenericWidgets GenericWidget { get; set; } // My guess at your 1st parameter
public int RowId { get; set; } // Additional parameter
}
Then you consume it on the parent like this:
private void AddWidgetToRow(AddWidgetArgs args) { ... } // Or async Task, etc.

How to use #bind-Value in a dynamic form?

I am using Blazor to create a dynamic form from the properties of my model.
I am using a for each loop to loop through the properties of the model.
public class SensorType
{
public int Id { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Sensor Type Name")]
public string Name { get; set; }
[Required]
[MaxLength(500)]
public string Description { get; set; }
[Required]
[MaxLength(2048)]
[Display(Name = "Datasheet URL")]
public string DatasheetUrl { get; set; }
}
I implemented this razor view, where I try to bind to public SensorType sensortype { get; set; }. But I need to bind to sensortype.property where property is whatever property the model has that is in the for each loop. But I cannot simply just call say #bind-Value="sensortype.property". Any ideas on how to do this? I don't want to have to manually type every field. Thanks!
<EditForm Model="#sensortype" OnValidSubmit="#SaveSensorType">
#foreach(var property in typeof(SensorType).GetProperties())
{
if(property.Name == "Id")
{
continue;
}
<div class="form-group row">
<label class="col-sm-2 col-form-label">#(GetAttribute(property, false)) </label> //This function get the name of the property in a human-readable way.
<div class="col-sm-10">//I would like to bind here to sensortype's property in the for each loop but sensortype.property gives me an error.
<InputTextArea class="form-control" #bind-Value="sensortype.property" value="sensortype.property.Name" placeholder="Description of the Type of Sensor" />
</div>
</div>
}
I am playing this days with blazor and I made a kind of DynamicForm, is not perfect but is working. I just want to show you my dynamic form as proof of concept, not at something which I wold use in production.
Basicly we want to write something like this:
<DynamicForm #bind-Model="MySensorType" />
//this will generate a form with fields for all properties of the model
So, on Index view let's create a property for MySensorType and some markup to see if the model is changing when the form fields are edited.
#page "/"
<div style="display:flex">
<div>
<DynamicForm #bind-Model="MySensorType" />
</div>
<div style="background:yellow;flex:1;margin:20px;">
<p>Id: #MySensorType.Id</p>
<p>Name: #MySensorType.Name</p>
<p>Description: #MySensorType.Description</p>
<p>Url: #MySensorType.DatasheetUrl</p>
</div>
</div>
#code {
public SensorType MySensorType { get; set; } = new SensorType();
public class SensorType
{
public int Id { get; set; } = 1;
public string Name { get; set; } = "Some Name";
public string Description { get; set; } = "Some Description";
public string DatasheetUrl { get; set; } = "This is a URL";
}
}
In order to generate automatically the fields, we need some kind of DynamicField.
Below component is for named "DynamicField"
<div>
<label>#Caption</label>
#if (Value is String sValue)
{
<input type="text" value="#sValue" #onchange="OnChange"/>
}
#if (Value is int iValue)
{
<input type="number" value="#iValue" #onchange="OnChange" />
}
</div>
#code {
[Parameter] public string Caption { get; set; }
[Parameter] public object Value { get; set; }
[Parameter] public EventCallback<object> ValueChanged { get; set; }
async void OnChange(ChangeEventArgs e)
{
await ValueChanged.InvokeAsync(e.Value);
}
}
Now, we can create the wrapper so called DynamicForm:
#typeparam T
#foreach (var p in Properties)
{
<DynamicField Value="#p.Value" Caption="#p.Key" ValueChanged="#((e)=>OnValueChanged(e,p.Key))"/>
}
#code{
[Parameter] public T Model { get; set; }
[Parameter] public EventCallback<T> ModelChanged { get; set; }
public Dictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();
protected override void OnInitialized()
{
var props = Model.GetType().GetProperties();
foreach (var p in props)
{
Properties.Add(p.Name, p.GetValue(Model));
}
}
void OnValueChanged(object e, string prop)
{
var p = Model.GetType().GetProperty(prop);
if (p.PropertyType == typeof(int))
{
var intValue = Convert.ToInt32(e);
p.SetValue(Model, intValue);
}
if (p.PropertyType == typeof(string))
{
p.SetValue(Model, e.ToString());
}
ModelChanged.InvokeAsync(Model);
}
}
What actually is happening here, we are using Reflection to get all properties of the model, send them to DynamicFields, and when those values are changed we set the new value to the model and call ModelChanged to send new values.
On my computer this works, and every time when I change a value, MySensorType is showing the new value on Index component.
You can see that I created dynamic fields only for Number and String, if you have DateTime or Select, you need to extend this DynamicField, for select will be more difficult.
By the way, on Index view you can put a button and call SaveChanges with your logic and use MySensorType.

Two-way binding through multiple components

I am trying to bind a model field to a control, I have it working to the form but I'm trying to refactor the components so that I can reused the control and it's attributes throughout the program.
Here are some snippets to try to portray my situation:
Sample Model
public class MyModel
{
public DateTime DateOpened { get; set; }
}
Used in MyModelFormBase.cs
public class MyModelFormBase : ComponentBase
{
protected MyModal data = new MyModel();
}
Used in MyModelForm.razor
<MyForm Model="#data" AddFunction="#InsertMyModel" DataDismiss="MyModelForm">
<MyDateInput InputId="dateOpened" InputName="Date Opened" #bind-InputValue="#data.DateOpened" />
</MyForm>
MyForm.razor, uses blazorstrap
<BSForm Model="#Model" OnValidSubmit="#AddFunction"
#ChildContent
<BSButton Color="Color.Primary"
ButtonType="ButtonType.Submit"
data-dismiss="#DataDismiss">
Submit</BSButton>
</BSForm>
#code {
[Parameter]
public Object Model { get; set;}
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback AddFunction { get; set; }
[Parameter]
public string DataDismiss { get; set; }
}
MyDateInput.razor, uses blazorstrap
<BSFormGroup>
<BSLabel For="#InputId">#InputName</BSLabel>
<BSInput InputType="InputType.Date" Id="#InputId"
#bind-Value="#InputValue"
ValidateOnChange="true"
#onchange="UpdateData"
/>
<BSFormFeedback For="#(() => InputValue)"/>
</BSFormGroup>
#code {
[Parameter]
public DateTime InputValue { get; set; }
[Parameter]
public EventCallback<DateTime> InputValueChanged { get; set; }
[Parameter]
public string InputId { get; set; }
[Parameter]
public string InputName { get; set; }
private async Task UpdateData()
{
await InputValueChanged.InvokeAsync(InputValue);
}
}
The default data provided by my service is correctly displayed in the control, so it's properly bound downwards, but doesn't propagate any changes back up to the model. :(
The goal is to be able to keep a MyDateInput Component that is repeated throughout the application that is bound 2 ways with the model regardless of layers of components it's passed through.
Any ideas?
It's really quite daunting to answer your question with so much code missing or unknown. However, I'll try to provide some assistance with the hope it can help you solve your ailments.
This code:
await InputValueChanged.InvokeAsync(InputValue);
I guess is a callback to update the value of the bound property MyModel.DateOpened , right ? If not it should be doing that. In any case, this property should be annotated with the PropertyAttribute attribute...
This may be the reason why two-way data binding is not working.
Note that your MyDateInput componet may also need to receive a ValueExpression value. I can't say that for sure, because not all your code is available, so take as a word of warning:
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
I'll put at the end of my answer a small sample to make this clear.
Are you sure this is working? The compiler should be barking:
#bind-Value="#InputValue" ValidateOnChange="true" #onchange="UpdateData"
telling you that you are using two onchange events. This should be, ordinarily
value="#InputValue" ValidateOnChange="true" #onchange="UpdateData", but I cannot be certain about it as I don't see how BSInput is implemented...
Note: BSForm is missing a closing tag before #ChildContent
Sample code:
RazorInputTextTest.razo
-----------------------
#page "/RazorInputTextTest"
<span>Name of the category: #category.Name</span>
<EditForm Model="#category">
<RazorInputText Value="#category.Name" ValueChanged="#OnValueChanged" ValueExpression="#(() => category.Name)" />
</EditForm>
#code{
private Category category { get; set; } = new Category { ID = "1", Name = "Beverages" };
private void OnValueChanged(string name)
{
category.Name = name;
}
}
RazorInputText.razor
--------------------
<div class="form-group">
<InputText class="form-control" #bind-Value="#Value"></InputText>
</div>
#code{
private string _value { get; set; }
[Parameter]
public string Value
{
get { return _value; }
set
{
if (_value != value) {
_value = value;
if (ValueChanged.HasDelegate)
{
ValueChanged.InvokeAsync(value);
}
}
}
}
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
}
Hope this helps...

DotVVM - Code only component DataSource property is null when collection is passed in

I have my own Accordion code-only component
Here is my view where I have repeater which loads list of article sections. Each article section have list of articles. So with that I want to archieve that every article section will have his own accordion, which will contain articles. Thats why I have it in repeater
<div class="box box-primary">
<dot:Repeater DataSource="{{value: AccordionList}}">
<ItemTemplate>
<coc:Accordion DataSource="{{value: Articles}}"></coc:Accordion>
</ItemTemplate>
</dot:Repeater>
</div>
Accordion code-only component. My DataSource is always null even when I clearly see, that AccordionList contains List of Articles which is never null, but is never passed into my DataSource. When I change type of AccordionList to ArticleListDTOand pass it directly into my Accordion component, it worked well, but thats not what I want.
public class Accordion : HtmlGenericControl
{
public Accordion() : base("div")
{
}
public static readonly DotvvmProperty DataSourceProperty;
static Accordion()
{
DataSourceProperty = DotvvmProperty.Register<List<ArticleListDTO>, Accordion>(c=>c.DataSource);
}
//DataSource is always null
public List<ArticleListDTO> DataSource
{
get => (List<ArticleListDTO>)GetValue(DataSourceProperty);
set => SetValue(DataSourceProperty, value);
}
protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context)
{
Attributes.Add("class", "accordion");
base.AddAttributesToRender(writer, context);
}
public void DataBind(IDotvvmRequestContext context)
{
Children.Clear();
foreach (var item in DataSource)
{
DataBindItem(this, item, context);
}
}....etc
ViewModel
public List<ArticleSectionListDTO> AccordionList { get; set; } = new List<ArticleSectionListDTO>();
public List<ArticleSectionListDTO> AccordionListUnsorted { get; set; } = new List<ArticleSectionListDTO>();
protected override void OnItemLoading()
{
AccordionListUnsorted = Task.Run(() => articleSectionFacade.GetAllNotModifiedArticleSections()).Result;
AccordionList = Task.Run(() => articleSectionFacade.CreateTree(AccordionListUnsorted, null)).Result.ToList();
}
DTOs - I deleted rest of properties to make it clear
public class ArticleListDTO
{
public string Name { get; set; }
public int? ParentArticleId { get; set; }
public bool HasCategories => AssignedToArticle?.Count > 0;
public List<ArticleListDTO> AssignedToArticle { get; set; }
//Can contain sub articles
public List<ArticleListDTO> Articles { get; set; } = new List<ArticleListDTO>();
}
public class ArticleSectionListDTO : ListDTO
{
public string Name { get; set; }
public int? ParentArticleSectionId { get; set; }
public bool HasCategories => AssignedToMenuItem?.Count > 0;
public List<ArticleSectionListDTO> AssignedToMenuItem { get; set; }
public List<ArticleListDTO> Articles { get; set; } = new List<ArticleListDTO>();
}
The problem is that Repeater probably uses the client-rendering mode (it's the default). When it renders the HTML, it renders something like this:
<div data-bind="foreach: something">
<!-- template -->
</div>
When the template is rendered, its DataContext is null (becasue the template must not contain data from an item - it is a template).
So you have two options here:
Turn on server rendering by adding RenderSettings.Mode="Server" to the Repeater.
Update your control so it doesn't call DataBind when DataContext is null.

Categories