blazor 如何将某种类型的列表传递给 EditorAttribute

blazor how pass list of some type to EditorAttribute

我正在构建一些通用表单生成器 所以我现在可以

public class  Model
{
 [Editor(typeof(CustomIntEditor), typeof(InputBase<>))]
  public int? testInt{ get; set; }
}

所以CustomIntEditor.razor

 @using System.Diagnostics.CodeAnalysis
 @using Microsoft.AspNetCore.Components.Forms
 @inherits InputBase<int?>

<select @attributes="AdditionalAttributes"
    type="number"
    class="@CssClass"
    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;
}

}

所以问题是这个编辑器如何传递一些 List<KeyValuePair<int,string>>

所以我可以构建这个选项/值,例如

 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; }

并循环构建列表,如

<option value =@key>@value</option>

------------更新------------

我现在就是这样

 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();
 }

但是如何将此 optionsList 传递给此 InputOptionsListSelect<> ?我不能自己实例化这个组件?据我检查,它需要无参数的构造函数。有什么想法吗?

我试过这样

  builder.AddContent(6,ListOptions); 

 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();
        }
    };

但它什么也没做。不知道它放在哪里;P --------------更新2---------------- 是的,我明白了,但这仍然解决了我的问题 我有一个适合你的地方是对的,但他仍然没有解决它。我没有地方放

请检查

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

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

看看他是怎么用的 InputEnumSelect - 这是我需要做的,但这不是枚举;P

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

这是我的概念,并在此基础上做了一些修改/添加了一些功能等 所以任何组件中都没有 .razor - 代码中的所有内容

并且初始组件使用是

<DynamicFormComponent 
TModel=DataX 
Model=d 
OnValidSubmitCallback="OnValidSubmit"
ShowValidationSummary=true
ShowValidationUnderField=true>

我发现我可能可以通过 AdditionalAtributes 做到这一点,因为它是 string/object 对 - 不确定它是否是存储列表的最佳位置,但我相信可以通过这种方式完成

感谢和问候!

我不知道您是如何生成表单控件或进行绑定的,但这里介绍了如何执行属性位。我已将其连接成标准形式,以便您可以查看实际代码。

@page "/"
<h3>GenericForm</h3>

<EditForm EditContext=this.editContext>
    <InputSelect class="form-select" @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;
        }
    }
}

更新

根据更新后的问题,这里有一个自定义组件,显示如何从 ValueExpression 获取模型信息和属性值并构建选项列表。

@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>
            }
        }
    };
}

以及修改后的演示页面:

@page "/"
<h3>GenericForm</h3>

<EditForm EditContext=this.editContext>
    <div class="p-2">
        <MySelect class="form-select" @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" },
        };
    }
}

第二次更新

这是在 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 
        if (BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out TValue? parsedValue))
        {
            result = parsedValue!;
            validationErrorMessage = "";
            return true;
        }

        // Map null/empty value to null if the bound object is nullable
        if (string.IsNullOrEmpty(value))
        {
            var nullableType = Nullable.GetUnderlyingType(typeof(TValue));
            if (nullableType != null)
            {
                result = default!;
                validationErrorMessage = "";
                return true;
            }
        }

        // The value is invalid => set the error message
        result = default;
        validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
        return false;
    }

    private void GetOptionList()
    {
#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.
        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;
            }
        }
#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.
    }

    // 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.");
    }
}