Blazor - How to bind list of classes to input - c#

In the //this works, the item.isSelected is updated but the set in private List<CheckboxModel> checkboxes won't run. And in the //this causes infinite loop in this component, nothing is updated and the foreach loop keeps running infinitely.
#foreach (var item in checkboxes)
{
//this works
#*<div class="checkbox" #key="item">
<input type="checkbox" id="#item.text" #bind="item.isSelected">
<label for="#item.text">#item.text</label>
</div>*#
Console.WriteLine(item.text);
//this causes infinite loop in this component
<Checkbox id="#item.text" #key="item" label="#item.text" #bind-isSelected="item.isSelected" />
}
private List<CheckboxModel> _checkboxes { get; set; } = new List<CheckboxModel>();
private List<CheckboxModel> checkboxes
{
get => _checkboxes;
set
{
// won't run
_checkboxes = value;
Console.WriteLine(string.Join(',', value.Where(x => x.isSelected).Select(x => x.text)));
}
}
This is CheckboxModel.cs
public class CheckboxModel
{
public bool isSelected { get; set; }
public string text { get; set; }
}
Checkbox.razor
<div class="checkbox">
<input type="checkbox" id="#id" #bind="#isSelected">
<label for="#id">#label</label>
</div>
#code {
[Parameter]
public string id { get; set; }
[Parameter]
public string label { get; set; }
private bool _isSelected { get; set; }
[Parameter]
public bool isSelected
{
get => _isSelected;
set
{
isSelectedChanged.InvokeAsync(value);
}
}
[Parameter]
public EventCallback<bool> isSelectedChanged { get; set; }
}

What I end up doing was changing Checkbox.razor to
CheckboxList.razor
#foreach (var item in items)
{
<div class="checkbox" #key="item">
<input type="checkbox" id="#item.text" checked="#item.isSelected" #onchange="args=> checkedChanged(item,args)">
<label for="#item.text">#item.text</label>
</div>
}
#code {
[Parameter]
public List<CheckboxModel> items { get; set; }
[Parameter]
public EventCallback<List<CheckboxModel>> itemsChanged { get; set; }
async Task checkedChanged(CheckboxModel item, ChangeEventArgs args)
{
item.isSelected = (bool)args.Value;
await itemsChanged.InvokeAsync(items);
}
}
and in the parent file
<CheckboxList #bind-items="checkboxes" />
#code {
private List<CheckboxModel> checkboxes { get; set; }
}
You can find 2 links in here.
One of them is simpler and the other one has a select all button
In short: I let the child component handle the logic of looping and handling the List passed into it. The child component passes the List back instead of just a bool which fixes the problem where it doesn't trigger the setter function when I update an item in a List.

Related

Blazor [Parameter] not updating input variables

I have a custom component InlineAutocomplete. No matter what I do my pic value and SelectedPIC value are always null when I do validation and unsure why it is.
The component does render correctly, but can't get my parameter back with the values set correctly.
Usage as follow:
<InlineAutocomplete TItem="PIC"
Value="#pic"
Selected="#SelectedPIC"
Data="#PICs"
Property="#((item) => item.PICNumber)" />
My component:
<div class="inline-autocomplete">
<input #oninput="(e)=> SuggestItem(e)" type="text" #bind-value="#Value" class="overlap inline-autocomplete-input " />
<input readonly class="overlap inline-autocomplete-label" value="#_suggestedValue" />
</div>
#code {
string _value;
string _suggestedValue;
[Parameter]
public string Value
{
get => _value;
set
{
if (value == null)
return;
_value = value;
}
}
[Parameter]
public List<TItem> Data { get; set; }
[Parameter]
public Func<TItem, string> Property { get; set; }
[Parameter]
public TItem Selected { get; set; }
void SuggestItem(ChangeEventArgs e)
{
if (string.IsNullOrEmpty(e.Value.ToString()))
return;
Value = e.Value.ToString().ToUpper();
if (string.IsNullOrEmpty(Value))
{
_suggestedValue = string.Empty;
}
else
{
var selectedItem = Data.Select(Property).FirstOrDefault(x => x.StartsWith(Value, StringComparison.OrdinalIgnoreCase));
_suggestedValue = !string.IsNullOrWhiteSpace(selectedItem) ? selectedItem : string.Empty;
Selected = Data.FirstOrDefault(x => string.Equals(Property(x), Value, StringComparison.OrdinalIgnoreCase));
StateHasChanged();
}
// State has changed
StateHasChanged();
}
}
The solution to this was to pass down a function that changes the property in the Parent instead of the Child.
Child:
[Parameter]
public Action<string, TItem> ValueChanged { get; set; }
...
#code {
void SomeFunction() {
ValueChanged(Value, Selected);
}
}
Parent:
<InlineAutocomplete ... ValueChanged="FireEvent" />
void FireEvent(string stringVal, MyClass c)
{
// ... Do Something
}

How to use either ?. or ?? to conditionally render Blazor template?

I want to simplify the following
#if (Template != null)
{
#Template
}
else
{
<span>No contents!</span>
}
with either ?? or ?.
Is it possible?
Attempt
My attempts below
#Template?.BeginInvoke()
#{Template==null? #Template : <span> No data to display! </span>}
#(Template??<span> No data to display! </span>)
produce red squiggly lines.
Edit
I think I need to submit the real scenario that I want to simplify.
#typeparam T
#if (Items == null)
{
if (NullTemplate != null)
{
#NullTemplate
}
else
{
<span style="color: red">Null...</span>
}
}
else if (Items.Count == 0)
{
if (EmptyTemplate != null)
{
#EmptyTemplate
}
else
{
<span style="color: red">Empty ...</span>
}
}
else
{
#HeaderTemplate
foreach (T item in Items)
{
#ItemTemplate(item)
}
}
#code{
[Parameter] public RenderFragment NullTemplate { get; set; }
[Parameter] public RenderFragment EmptyTemplate { get; set; }
[Parameter] public RenderFragment HeaderTemplate { get; set; }
[Parameter] public RenderFragment<T> ItemTemplate { get; set; }
[Parameter] public List<T> Items { get; set; }
}
I don't want to buffer the incoming values to ****Template properties with private fields, pre-process the fields before rendering them (the fields) to HTML. In other words, no additional code in #code{} directive are allowed.
It could work (I think) when you store the <span style="color: red">Null...</span> parts as RenderFragments. Would that be acceptable?
That would move this logic to the constructor or the SetParameters method.
[Parameter] public RenderFragment NullTemplate { get; set; } = builder => StubTemplate(builder, "Null");
[Parameter] public RenderFragment EmptyTemplate { get; set; } = builder => StubTemplate(builder, "Empty");
private static void StubTemplate(RenderTreeBuilder __builder, string what)
{
<span style="color: red">#what</span>
}
Note that the __builder name is required.
And then you don't need any conditional code in the main component anymore:
#if (Items == null)
{
#NullTemplate
}
Alternatively, you can omit the property initialization and do :
#(EmptyTemplate ?? (builder => StubTemplate(builder, "Empty")))
but I would get dizzy of all the ().

How to get return value from EventCallback in blazor?

My situation is this: I'm trying to implement and Autocomplete.
The Autocomplete will have a Parameter that will receive a string and return a IEnumerable<TValue>.
Here is an example of what I'm trying to do
Autocomplete.razor
#code {
[Parameter]
public SOME_TYPE GetItems { get; set; }
async void Foo(){
IEnumerable<TValue> items = await GetItems(SomeString);
// do something with items
}
}
ParentComponent.razor
<Autocomplete TValue="SomeEntity"
GetItems="#GetItems" />
#code {
SOME_TYPE GetItems(string name) {
IEnumerable<SomeEntity> entity = await GetEntitys(name);
return entity;
}
}
The problem is that I don't know what to put in SOME_TYPE. Should I use EventCallback? Action? What should I use?
I tried using EventCallback but looks like I can't get a return value from EventCallback? I have no idea.
I just find out how to do it, I should use Func<string, Task<IEnumerable<TValue>>>.
[Parameter]
public Func<string, Task<IEnumerable<TValue>>> GetItems { get; set; }
And
public async Task<IEnumerable<Employee>> GetItems(string name) {
IEnumerable<SomeEntity> entity = await GetEntitys(name);
return entity;
}
You can also use argument of EventCallback for that:
public class CancelArg
{
public bool Value { get; set; }
}
[Parameter]
public EventCallback<CancelArg> OnFoo { get; set; }
async Task CallerInChild()
{
var cancel = new CancelArg();
await OnFoo.InvokeAsync(cancel);
if (cancel.Value)
{
// cancelled ...
}
}
void HandlerInParent(CancelArg cancel)
{
cancel.Value = true;
}
In a child component:
<div class="form-group">
<label for="ddSetStatus">Status</label>
<InputSelect class="form-control" id="ddSetStatus" #bind-Value="#Status" #oninput="#((e) => ChangedSelection(e))">
<option value="1">Draft </option>
<option value="2">Published </option>
<option value="3">Archived </option>
</InputSelect>
</div>
#code
{
[Parameter] public EventCallback<int> OnStatusSelected { get; set; }
[Parameter] public int Status { get; set; }
private async Task ChangedSelection(ChangeEventArgs args)
{
await OnStatusSelected.InvokeAsync(int.Parse(args.Value.ToString()));
}
}
and then consuming the selected value in a parent page or component:
create a component (called DocStatus in this example) and define the event handling method
in markup area:
<DocStatus Status="#Status" OnStatusSelected="StatusSelectionHandler"/>
in the code area:
private void StatusSelectionHandler(int newValue)
{
Status = newValue;
}

Dynamically binding input-text to class/object propertys using Blazor

Im trying to build a dynamic list of input field for properties inside a class using Blazor but cant figur out how to bind/link the content of a input box to a property of a class. (the class have can have a large number of public props, not only Name and Description as in the below example, they are not always of the type "string")
lets say that I have this class/model:
public class customer{
public string Name { get; set; }
public int Age { get; set; }
public string Description { get; set; }
}
I got this blazor component (updateC.razor):
#inherits CLogic
#if (nfo != null)
{
#foreach (var obj in nfo)
{
<input type="text" class="form-control"
bind=#SCustomer.GetType().GetProperty(obj.ToString())/>
}
}
and finally Clogic:
public class Clogic: ComponentBase{
[Parameter]
public Customer SCustomer { get; set; } = new Customer();
[Parameter]
public PropertyInfo[] nfo { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
nfo = SCustomer.GetType().GetProperties();
StateHasChanged();
}
}
}
This is suppose to bind changes made in each input field to the correct property in the current instance of SCustomer (when input is made it is suppose to update the correct property of the class/object). This is not working, values inside of SCustomer are not changed after input are done. I'm guessing that i'm going about this completely wrong but can't seem to figure out how to make this work and can't find any examples doing this.
#foreach (var propertyInfo in nfo)
{
<input type="text" class="form-control"
value="#propertyInfo.GetValue(SCustomer)"
#onchange="#((ChangeEventArgs __e) =>
propertyInfo.SetValue(SCustomer, __e.Value.ToString()))" />
}
#foreach(propertyInfo in nfo)
{
<label>#propertyInfo.Name</label>
<input type="text" value="#propertyInfo.GetValue(SCustomer)" #onchange="#((ChangeEventArgs __e) =>
propertyInfo.SetValue(SCustomer,Convert.ChangeType(__e.Value,
propertyInfo.PropertyType,null))"/>
}
I finally found a way of doing this, here is my solution:
I created a helper class:
public class PropHolder
{
[Parameter]
public PropertyInfo info { get; set; }
[Parameter]
public string type { get; set; }
public PropHolder(PropertyInfo nfo, string propType)
{
info = nfo;
type = propType;
}
}
then i created a dictionary of this class and some cheking functions (this is inside of Clogic)
[Parameter]
public Dictionary<int, PropHolder> Props{ get; set; }
public void GetAllProps()
{
Props = new Dictionary<int, PropHolder>();
//nfo = SCustomer.GetType().GetProperties();
int Cid = 0;
foreach (PropertyInfo pif in SCustomer.GetType().GetProperties())
{
Props[Cid] = new PropHolder(pif, pif.PropertyType.Name);
Cid++;
}
}
public string CheckName(PropHolder propertyInfo)
{
if (propertyInfo.GetType() == typeof(PropHolder))
{
return propertyInfo.type;
}
else
{
return propertyInfo.GetType().Name.ToString();
}
}
public PropertyInfo getInfo(PropHolder propertyInfo)
{
if (propertyInfo.GetType() == typeof(PropHolder))
{
return propertyInfo.info;
}
else
{
return null;
}
}
and finally im able to loop over the keys of my dictionary and bind all values correctly (got lots of help figuring this out from the answere given by: "agua from mars")
here is the updateC.razor content:
#if (Props != null)
{
#foreach (int key in Props.Keys)
{
var pinfo = Props[key];
#if (CheckName(pinfo) == "String")
{
<input type="text" class="form-control" value=#(getInfo(pinfo).GetValue(SCustomer)) #onchange="#((ChangeEventArgs __e) => getInfo(pinfo).SetValue(SCustomer, __e.Value.ToString()))" />
}
}
}
this gives me a input box for every prop that is of the type String, if additional types are to be handled it can now easily be added. This is probably not the best sulution but it do work. I will update the answere if any better working solutions are posted.
I solved this using generics. This lets each input type pass on a generic type which defines what type it wants to use.
<input #bind="Name.Value" />
<input #bind="Age.Value" />
<div><code>Name</code>: #Name.Value</div>
<div><code>Age</code>: #Age.Value</div>
#code {
private FormField<string> Name { get; set; } = new FormField<string>();
private FormField<int> Age { get; set; } = new FormField<int>();
public class Form
{
public ICollection<IFormField> Fields { get; set; }
}
public interface IFormField
{
public int ControlType { get; set; }
}
public class FormField<T> : IFormField
{
public int ControlType { get; set; }
public T Value { get; set; }
}
}
Here it is on BlazorFiddle: https://blazorfiddle.com/s/wen1g26q

In Blazor, How to #bind and then fire #onchange in a dynamic model

Model:
public class FiltersModel
{
public CheckBoxListWithTitle Brand { get; set; }
}
public class CheckBoxListWithTitle
{
public List<FilterCheckBox> CheckBoxes { get; set; }
}
public class FilterCheckBox
{
public string Value { get; set; }
public bool Checked { get; set; }
}
Razor:
#foreach (var item in Model.Brand.CheckBoxes)
{
<label>
#item.Value
<input type="checkbox" #onchange="#FilterChangedBrand" />
</label>
}
#code:
public FiltersModel Model { get; set; } // Initialized in OnParametersSet
private void FilterChangedBrand(UIChangeEventArgs e)
{
string newCheckedBrand = e.Value.ToString();
// Now How to Find and Set the relevant Model property to newCheckedBrand
FiltersChanged?.Invoke(Model);
}
How to Find and Set the relevant Model property to newCheckedBrand in the FilterChangedBrand method.
Or Use #bind="#item.Checked" in the checkbox markup and then raise an event when the checked state for one of checkboxes changes?
Since there no way how you can use #bind and #onchange you have to make changes purely in the code. Simplest way for you to do that is to use lambda to capture item
Razor
#foreach (var item in Model.Brand.CheckBoxes)
{
<label>
#item.Value
<input type="checkbox" #onchange="(e) => FilterChangedBrand(item, e)" />
</label>
}
#code
public FiltersModel Model { get; set; } // Initialized in OnParametersSet
public event Action<FiltersModel> FiltersChanged;
private void FilterChangedBrand(FilterCheckBox item, ChangeEventArgs e)
{
// here you do work of #bind
item.Checked = !item.Checked;
string newCheckedBrand = e.Value.ToString();
// Now How to Find and Set the relevant Model property to newCheckedBrand
FiltersChanged?.Invoke(Model);
}
Alternative and more complicated way, which may helps if you want reuse your UI with for example WPF is to place that event cascading in the model itself.
public class CheckBoxListWithTitle
{
private List<FilterCheckBox> items = new List<FilterCheckBox>();
public IReadOnlyList<FilterCheckBox> CheckBoxes => items.AsReadOnly();
public event EventHandler ModelChanged;
public void Add(FilterCheckBox item)
{
item.CheckedChanged += this.Item_CheckedChanged;
this.items.Add(item);
}
private void Item_CheckedChanged(object sender, EventArgs e)
{
ModelChanged.Invoke(this, EventArgs.Empty);
}
}
public class FilterCheckBox
{
public string Value { get; set; }
public bool Checked { get; set; }
public event EventHandler CheckedChanged;
}
as you see CheckBoxListWithTitle will handle propagation of required events. in Razor you only subscribe to CheckBoxListWithTitle.ModelChanged
another option is to use in your bound object the INotifyPropertyChanged interface.
public class RowToApprove : INotifyPropertyChanged
{
private bool _approved;
public string AddressLine { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Approved
{
get => _approved;
set
{
if (value == _approved) return;
_approved = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
and then in your page, register to ItemOnPropertyChanged event.
item.PropertyChanged += ItemOnPropertyChanged;
and do your work there:
private void ItemOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
//Do something
}
your item could look like this:
<input type="checkbox" #bind-value="#(context.Approved)" />

Categories