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
Related
I am developing a custom blazor select component. The component has a property named "Value" which in most cases is a string value which I can bind to using #bind-Value without any issues.
However in some situations the property which it binds to may be an integer (outside the component) in which case I need to convert from a string to an integer. I initially assumed simply using Int32.Parse would accomplish this as follows:
Select.razor.cs
namespace Accounting.Web.Components
{
public partial class Select
{
[Parameter]
public string Value { get; set; } = default!;
[Parameter]
public EventCallback<string>? ValueChanged { get; set; }
public void OnClick()
{
Value = "2";
ValueChanged?.InvokeAsync(Value);
}
}
}
Index.razor.cs
<Select Id="InvestmentEntitySelect" #bind-Value="#Int32.Parse(AddDto.InvestmentEntityId)">
#foreach (var entity in Entities)
{
<SelectOption Value="#entity.InvestmentEntityId.ToString()">#entity.Name</SelectOption>
}
</Select>
AddDto.cs
namespace Accounting.Shared.Dtos.Request.Investment
{
public class AddDto
{
[Required]
public int? InvestmentEntityId { get; set; } = 0;
[Required]
public string Description { get; set; } = String.Empty;
}
}
The line of interest is:
#bind-Value="#Int32.Parse(AddDto.InvestmentEntityId)"
But unfortunately this produces an error:
Argument 1: cannot convert from 'int' to 'System.ReadOnlySpan'
So how can I converted the bind value to an integer in the above example?
You need to create a custom input select like below and override the TryParseValueFromString method of it:
public class CustomInputSelect<TValue> : InputSelect<TValue>
{
protected override bool TryParseValueFromString(
string? value,
out TValue result,
out string? validationErrorMessage)
{
if (typeof(TValue) == typeof(int))
{
if (int.TryParse(value, out var resultInt))
{
result = (TValue)(object)resultInt;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage =
$"The selected value {value} is not a valid number.";
return false;
}
}
else
{
return base.TryParseValueFromString(value, out result,
out validationErrorMessage);
}
}
}
You have to do it like this:
<Select Id="InvestmentEntitySelect" Value="#AddDto.InvestmentEntityId.ToString()" ValueChanged="OnValueChanged">
#foreach (var entity in Entities)
{
<SelectOption Value="#entity.InvestmentEntityId.ToString()">#entity.Name</SelectOption>
}
</Select>
#code {
private void OnValueChanged(string selectedValue)
{
AddDto.InvestmentEntityId = int.Parse(selectedValue);
}
}
im learning blazor and wanted to build some small dynamic form generator
i known that there is already something like VxFormGenerator but wanted to learn by myself a bit - at least for simple forms purposes.
so i have it like this:
DynamicFormsComponent:
<EditForm Model = "#Params" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator/>
#if(Params != null ) #foreach (var field in Params.FormFields)
{
<div class="mb-3">
<label for= "#field.Id">#field.Label :</label>
#switch (field.Type)
{
case FormFieldType.Text:
{
<InputText id="#field.Id" #bind-Value="#field.StrValue" placeholder="#field.PlaceHolder" class="form-control"></InputText>
break;
}
case FormFieldType.Number:
{
<InputNumber id="#field.Id" #bind-Value="#field.IntValue" placeholder="#field.PlaceHolder" class="form-control"> ></InputNumber>
break;
}
case FormFieldType.Date:
{
<InputDate id="#field.Id" #bind-Value="#field.DateValue" placeholder="#field.PlaceHolder" class="form-control"></InputDate>
break;
}
default:
{
break;
}
}
</div>
}
<ValidationSummary></ValidationSummary>
<button type="submit" class="btn btn-primary">#Params?.SendButtonText</button>
public partial class DynamicFormComponent:ComponentBase
{
[Parameter]
public DynamicFormParams Params { get; set; } = new DynamicFormParams();
[Parameter]
public EventCallback<DynamicFormParams> OnValidSubmitCallback { get; set; }
void OnValidSubmit()
{
Console.WriteLine("onValidSubmit");
if (OnValidSubmitCallback.HasDelegate ) OnValidSubmitCallback.InvokeAsync(Params);
//NavigationManager.navigateto.....
}
}
public class DynamicFormParams
{
public List<DynamicFormField> FormFields { get; set; } = new List<DynamicFormField>();
public string FormTitle { get; set; } = string.Empty;
public string SendButtonText { get; set; } = "Send";
}
public class DynamicFormField
{
public string? Label { get; set; }
public string Id { get; set; } = Guid.NewGuid().ToString();
public string PlaceHolder { get; set; } = string.Empty;
public FormFieldType? Type { get; set; }
public string? StrValue { get; set; }
public int? IntValue { get; set; }
public DateTime? DateValue { get; set; }
}
public enum FormFieldType
{
Text,
Number,
Date
}
so the usage would be
<DynamicFormComponent Params="#p" OnValidSubmitCallback=#onChildFormSubmit ></DynamicFormComponent>
DynamicFormParams p = new DynamicFormParams()
{
FormTitle = "test form Title",
SendButtonText = "WyĆlij",
FormFields = new List<DynamicFormField>()
{
new DynamicFormField()
{
Label="testLabelStr",
Id="anyid-notGuId",
StrValue="a",
PlaceHolder="asdadsad",
Type=FormFieldType.Text
},
new DynamicFormField()
{
Label="testLabelInt",
Type=FormFieldType.Number,
PlaceHolder="enter nr"
},
new DynamicFormField()
{
Label="testLabelDate",
Type=FormFieldType.Date,
DateValue=DateTime.Parse("2021-04-01")
}
}
};
private void onChildFormSubmit(DynamicFormParams pp)
{
Console.WriteLine("from local variable");
Console.WriteLine(JsonSerializer.Serialize(p));
Console.WriteLine("from event arg");
Console.WriteLine(JsonSerializer.Serialize(pp));
}
and the question is:
how with this approach i can use form validation ?
i have no clasic'model' so probably would need something like add 'list of validators ' to my DynamicFormField class
and somehow force DataAnnotationsValidator to use this list ? is it possible withoud clasic 'model' and 'data annotations attributes' on it ?
thanks and regards
The only way to validate form without a model is to use the Blazorise validation system. https://blazorise.com/docs/components/validation.
PS. I'm a Blazorise creator.
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 ().
I have a class that contains another poco class with simple get set properties:
public class PersonalInformation {
public string FirstName { get; set; }
public string FirstSomethingElse { get; set; }
}
I would like to find out if the current instance's PersonalInformation.FirstName has a value. I can't figure out how to obtain it via reflection:
foreach (PropertyInfo property in this.PersonalInformation.GetType().GetProperties())
{
if (property.Name.Contains("First"))
{
if (property.GetValue(XXX, null) != null)
do something...
}
}
The instance I have is "this", which does not work, neither does this.PersonalInformation. What am I doing wrong?
Thank you for your response,
Aldo
Addendum: I'm using ASP.NET MVC3. In my razor view I can do the following very easily:
foreach (var property in Model.PersonalInformation.GetType().GetProperties())
{
<div class="editor-line">
#if (property.Name != null)
{
<label>#(property.Name)</label>
#Html.Editor(property.Name)
}
</div>
}
there is a property.Value member that returns the current value of the field. This field comes from a poco class, as you see above. What would be the equivalent code in the code-behind?
this.PersonalInformation certainly should work. After all, that's the target you're talking about.
Sample code:
using System;
using System.Reflection;
public class PersonalInformation {
public string FirstName { get; set; }
public string FirstSomethingElse { get; set; }
}
public class Foo
{
public PersonalInformation PersonalInformation { get; set; }
public void ShowProperties()
{
foreach (var property in this.PersonalInformation
.GetType()
.GetProperties())
{
var value = property.GetValue(this.PersonalInformation, null);
Console.WriteLine("{0}: {1}", property.Name, value);
}
}
}
class Test
{
static void Main()
{
Foo foo = new Foo {
PersonalInformation = new PersonalInformation {
FirstName = "Fred",
FirstSomethingElse = "XYZ"
}
};
foo.ShowProperties();
}
}
Although if you just "want to find out if the current instance's PersonalInformation.FirstName has a value" then I don't see why you're using reflection...
GetProperties returns a PropertyInfo[], not a single PropertyInfo.
How can I get my properties from a Model into my View with a foreach?
I know that I could use #Html.EditorFor(model => model.ID) but in my case this is not possible because I use one View for different Models (inherit from a BaseModel).
Model:
public class MyModel : IEnumerable
{
private PropertyInfo[] propertys
{
get
{
if (propertys != null) return propertys;
string projectName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
Type classtype = Type.GetType(string.Format("{0}.Models.{1}", projectName, FQModelname));
PropertyInfo[] properties = classtype.GetProperties();
return properties;
}
}
public int ID { get; set; }
public string Name { get; set; }
//...
public IEnumerator GetEnumerator()
{
return propertys.GetEnumerator();
}
}
RazorView:
#foreach (var property in Model)
{
// [Error] need Typeargument...?
#Html.EditorFor(property);
}
Have you tried #Html.EditorForModel() instead of #Html.EditorFor() ??
This could be done stronger typed but this is a quick implementation of the idea at least, you'll want to refine some of the concepts and get something working for your specific project.
void Main()
{
BaseModel baseModelTest = new Concrete() { Test = "test property" };
foreach ( var property in baseModelTest.EnumerateProperties())
{
var value = baseModelTest.GetPropertyValue(property.Name);
value.Dump();
}
}
public class EnumeratedProperty
{
public string Name { get; private set; }
public Type Type { get; private set; }
public EnumeratedProperty(string PropertyName, Type PropertyType)
{
this.Name = PropertyName;
this.Type = PropertyType;
}
}
public abstract class BaseModel
{
protected IEnumerable<PropertyInfo> PropertyInfoCache { get; set; }
protected IEnumerable<EnumeratedProperty> EnumeratedPropertyCache { get; set; }
protected BaseModel()
{
PropertyInfoCache = this.GetType().GetProperties();
EnumeratedPropertyCache = PropertyInfoCache.Select(p=> new EnumeratedProperty(p.Name,p.GetType()));
}
public IEnumerable<EnumeratedProperty> EnumerateProperties()
{
return EnumeratedPropertyCache;
}
public object GetPropertyValue(string PropertyName)
{
var property = PropertyInfoCache.SingleOrDefault(i=>i.Name==PropertyName);
if(property!=null)
return property.GetValue(this,null);
return null;
}
}
public class Concrete : BaseModel
{
public string Test { get; set; }
}
....
public static class ExtensionMethods
{
public static MvcHtmlString EditorForProperty(this HtmlHelper html, BaseModel Model, EnumeratedProperty property)
{
// invoke the appropriate Html.EditorFor(...) method at runtime
// using the type info availible in property.Type
return ...
}
}
....
#foreach (var property in Model.EnumerateProperties())
{
// call the new extention method, pass the EnumeratedProperty type
// and the model reference
#Html.EditorForProperty(Model,property);
}