im building some generic forms builder so im at the point where i can
public class Model
{
[Editor(typeof(CustomIntEditor), typeof(InputBase<>))]
public int? testInt{ get; set; }
}
so CustomIntEditor.razor
@using System.Diagnostics.CodeAnalysis
@using Microsoft.AspNetCore.Components.Forms
@inherits InputBase<int?>
<select @attributes="AdditionalAttributes"
type="number"
value="@CurrentValueAsString"
@onchange="e => CurrentValueAsString = (string?)e.Value">
<option value =1>Choice 1</option>
<option value =2>Choice 2</option>
<option value =3 >Choice 3</option>
@code {
protected override string FormatValueAsString(int? value)
{
if (value != null) return value.ToString()!; else return string.Empty;
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out int? result, [NotNullWhen(false)] out string? validationErrorMessage)
{
validationErrorMessage = null;
if (value != null) result = int.Parse(value!); else result = null;
return true;
}
}
so the question is how to this Editor pass some List<KeyValuePair<int,string>>
so i could build this options / values like
List<KeyValuePair<int,string>> L = new List<KeyValuePair<int,string>>(){
{1,"asd"},
{2,"bsd"}
}
.
.
.
[Editor(typeof(CustomIntEditor), typeof(InputBase<>),L)]
public int? testInt{ get; set; }
and loop on it to build the list like
<option value =@key>@value</option>
---------- update-----------
i have it now like this
private RenderFragment CreateOptionsListComponent() => builder =>
{
var optionsListAttribute = (OptionsListAttribute?)property.GetCustomAttributes(typeof(OptionsListAttribute), false).FirstOrDefault();
if (optionsListAttribute is not null)
{
var optionsList = (SortedDictionary<int, string>?)typeof(TModel).GetProperty(optionsListAttribute.List, typeof(SortedDictionary<int, string>))?.GetValue(model!);
}
builder.OpenComponent(0,typeof(InputOptionsListSelect<>).MakeGenericType(property!.PropertyType));
builder.AddAttribute(1, "Value", Value);
builder.AddAttribute(2, "ValueChanged", changeHandler);
builder.AddAttribute(3, "ValueExpression", lambdaExpression);
builder.AddAttribute(4, "id", FieldId());
builder.AddAttribute(5, "class", "form-control");
builder.CloseComponent();
}
but how pass this optionsList to this InputOptionsListSelect<> ? i canot instanciete this component myself ? it need parameterless ctor as far as i checked. any idea ?
i did try like this
builder.AddContent(6,ListOptions);
and
private RenderFragment ListOptions => (__builder) =>
{
foreach(var option in this.optionsList!)
{
__builder.OpenElement(7, "option");
__builder.AddAttribute(8, "value", option.Key);
__builder.AddContent(9, option.Value);
__builder.CloseElement();
}
};
but it did nothing at all. no idea where it is puting that ;P ----------------update2---------------- yes i see but still this noit solve my issue i havem o place for you are right but shis still does not solve it. i have nowhere place for <MySelect @bind-Value=modelData.Id />
please check
and
check how he uses InputEnumSelect - this is what i need to do but this is not an enum ;P
this is the concept that i took and base on this i do some modify / adding some features etc so no .razor in any of components - everything from the code
and initial component usage is
<DynamicFormComponent
TModel=DataX
Model=d
OnValidSubmitCallback="OnValidSubmit"
ShowValidationSummary=true
ShowValidationUnderField=true>
i found that i probably can do this via AdditionalAtributes cause it is string/object pair - not sure if it is best place for storing the list but it can be done this way i belive
thanks and regards !
CodePudding user response:
I don't know how you're generating your form controls or doing your bindings but here's how to do the Attribute bit. I've wired it into a standard form so you can see the code in action.
@page "/"
<h3>GenericForm</h3>
<EditForm EditContext=this.editContext>
<InputSelect @bind-Value=modelData.Id>
@this.ListOptions
</InputSelect>
</EditForm>
@code {
public TestModel modelData = new TestModel() { Id = 2 };
private EditContext? editContext;
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(modelData);
return base.OnInitializedAsync();
}
private SortedDictionary<int, string> GetFieldList(string fieldName)
{
var list = new SortedDictionary<int, string>();
var typeInfo = this.modelData.GetType();
var prop = typeInfo.GetProperty(fieldName);
var editorAttr = prop?.GetCustomAttributes(true).ToList().SingleOrDefault(item => item is OptionListAttribute);
if (editorAttr is not null)
{
OptionListAttribute attr = (OptionListAttribute)editorAttr;
var obj = typeInfo.GetProperty(attr.List)?.GetValue(modelData);
if (obj is not null)
list = (SortedDictionary<int, string>)obj;
}
return list;
}
private RenderFragment ListOptions => (__builder) =>
{
@foreach (var option in this.GetFieldList("Id"))
{
<option value="@option.Key">@option.Value</option>
}
};
public class TestModel
{
[OptionList("LookupList")]
public int Id { get; set; }
public SortedDictionary<int, string> LookupList { get; set; } = new SortedDictionary<int, string>()
{
{ 1, "UK" },
{ 2, "France" },
{ 3, "Spain" },
};
}
[AttributeUsage(AttributeTargets.Property)]
public class OptionListAttribute : Attribute
{
public string List { get; set; }
public OptionListAttribute(string list)
{
List = list;
}
}
}
Update
Based on the updated Question here's a custom component that shows how to get the model information and attribute value from the ValueExpression
and build the option list.
@using System.Linq.Expressions
@typeparam TValue
<InputSelect @attributes=UserAttributes Value="@this.Value" ValueChanged=this.ValueChanged ValueExpression=this.ValueExpression!>
@this.ListOptions
</InputSelect>
@code {
[Parameter] public TValue? Value { get; set; }
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
private string? fieldName;
private object? model;
private SortedDictionary<TValue, string>? optionList;
protected override void OnInitialized()
{
base.OnInitialized();
if (this.ValueExpression is null)
throw new NullReferenceException("You must set a ValueExpression for the component");
// As we get the ValueExpression we can use it to get the property name and the model
ParseAccessor<TValue>(this.ValueExpression, out model, out fieldName);
// And then get the OptionList
GetOptionList();
}
private void GetOptionList()
{
optionList = new SortedDictionary<TValue, string>();
var typeInfo = this.model?.GetType();
if (typeInfo is not null)
{
var prop = typeInfo.GetProperty(fieldName!);
var editorAttr = prop?.GetCustomAttributes(true).ToList().SingleOrDefault(item => item is OptionListAttribute);
if (editorAttr is not null)
{
OptionListAttribute attr = (OptionListAttribute)editorAttr;
var obj = typeInfo.GetProperty(attr.List)?.GetValue(model);
if (obj is null)
throw new ArgumentException("The provided field must implement the OptionList Attribute.");
optionList = (SortedDictionary<TValue, string>)obj;
}
}
}
// This method takes the Expression provided in ValueExpression and gets the model object and the name of the field referenced
private static void ParseAccessor<T>(Expression<Func<T>> accessor, out object model, out string fieldName)
{
var accessorBody = accessor.Body;
if (accessorBody is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert && unaryExpression.Type == typeof(object))
accessorBody = unaryExpression.Operand;
if (!(accessorBody is MemberExpression memberExpression))
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
fieldName = memberExpression.Member.Name;
if (memberExpression.Expression is ConstantExpression constantExpression)
{
if (constantExpression.Value is null)
throw new ArgumentException("The provided expression must evaluate to a non-null value.");
model = constantExpression.Value;
}
else if (memberExpression.Expression != null)
{
var modelLambda = Expression.Lambda(memberExpression.Expression);
var modelLambdaCompiled = (Func<object?>)modelLambda.Compile();
var result = modelLambdaCompiled();
if (result is null)
throw new ArgumentException("The provided expression must evaluate to a non-null value.");
model = result;
}
else
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
}
private RenderFragment ListOptions => (__builder) =>
{
@if (this.optionList is not null)
{
@foreach (var option in this.optionList)
{
<option value="@option.Key.ToString()">@option.Value</option>
}
}
};
}
And a modified demo page:
@page "/"
<h3>GenericForm</h3>
<EditForm EditContext=this.editContext>
<div >
<MySelect @bind-Value=modelData.Id />
</div>
</EditForm>
@code {
public TestModel modelData = new TestModel() { Id = 2 };
private EditContext? editContext;
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(modelData);
return base.OnInitializedAsync();
}
public class TestModel
{
[OptionList("LookupList")]
public int Id { get; set; }
public SortedDictionary<int, string> LookupList { get; set; } = new SortedDictionary<int, string>()
{
{ 1, "UK" },
{ 2, "France" },
{ 3, "Spain" },
};
}
}
Second Update
Here's the code from above implemented in a version of InputEnumSelect
public sealed class InputListSelect<TValue> : InputBase<TValue>
{
private string? fieldName;
private object? model;
#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
private SortedDictionary<TValue, string>? optionList;
#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
protected override void OnInitialized()
{
base.OnInitialized();
if (this.ValueExpression is null)
throw new NullReferenceException("You must set a ValueExpression for the component");
// As we get the ValueExpression we can use it to get the property name and the model
ParseAccessor<TValue>(this.ValueExpression, out model, out fieldName);
// And then get the OptionList
GetOptionList();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "select");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string?>(this, value => CurrentValueAsString = value, CurrentValueAsString, culture: null));
if (optionList is not null)
{
foreach (var option in optionList)
{
builder.OpenElement(5, "option");
builder.AddAttribute(6, "value", option.Key?.ToString());
builder.AddContent(7, option.Value);
builder.CloseElement();
}
}
builder.CloseElement(); // close the select element
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
// Let's Blazor convert the value for us