Home > Software design >  blazor how pass list of some type to EditorAttribute
blazor how pass list of some type to EditorAttribute

Time:04-29

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

https://github.com/meziantou/Meziantou.Framework/blob/main/src/Meziantou.AspNetCore.Components/GenericForm.razor

and

https://github.com/meziantou/Meziantou.Framework/blob/main/src/Meziantou.AspNetCore.Components/GenericFormField.cs

check how he uses InputEnumSelect - this is what i need to do but this is not an enum ;P

https://github.com/meziantou/Meziantou.Framework/blob/main/src/Meziantou.AspNetCore.Components/InputEnumSelect.cs

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            
  • Related