如何使用 Blazor 动态构建 Enum select?

How can I dynamically build an Enum select with Blazor?

我正在尝试使用 Blazor 构建通用表单组件。目前,除了枚举选择之外,所有其他输入类型都在工作。我认为发生这种情况是因为编译器在尝试添加表达式和回调函数时不知道具体的枚举类型:

public partial class GenericForm<ViewModel> : ComponentBase where ViewModel : new()
{
    [Parameter]
    public ViewModel Model { get; set; }
    public readonly PropertyInfo[] Properties = typeof(ViewModel).GetProperties();
    [Parameter] public EventCallback<ViewModel> OnValidSubmit { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (Model == null)
        {
            Model = new ViewModel();
        }
        await base.OnInitializedAsync();
    }
    public RenderFragment CreateComponent(PropertyInfo property) => builder =>
    {
        var typeCode = Type.GetTypeCode(property.PropertyType);
        if (property.PropertyType.IsEnum)
        {
            BuildEnumComponent(builder,property);
        }
        else
        {
            switch (typeCode)
            {
                case TypeCode.Int32:
                    BuildComponent<double>(property, builder, typeof(InputNumber<double>));
                    break;
                case TypeCode.Int64:
                    BuildComponent<long>(property, builder, typeof(InputNumber<long>));
                    break;
                case TypeCode.Int16:
                    BuildComponent<int>(property, builder, typeof(InputNumber<int>));
                    break;
                case TypeCode.Decimal:
                    BuildComponent<decimal>(property, builder, typeof(InputNumber<decimal>));
                    break;
                case TypeCode.String:
                    BuildComponent<string>(property, builder, typeof(InputText));
                    break;
                case TypeCode.Boolean:
                    BuildComponent<bool>(property, builder, typeof(InputCheckbox));
                    break;
                case TypeCode.DateTime:
                    BuildComponent<DateTime>(property, builder, typeof(InputDate<DateTime>));
                    break;
                default:
                    Console.WriteLine("Unknown property type");
                    break;
            }
        }
        
    };

    private void BuildEnumComponent(RenderTreeBuilder builder,PropertyInfo property)
    {
        Guid id = Guid.NewGuid();
        builder.AddMarkupContent(1, $"<label for=\"{id}\">{property.Name}</label>");
        builder.OpenElement(2, "select");
        builder.AddAttribute(3, "id", id.ToString());
        builder.AddAttribute(4, "Value", Enum.GetValues(property.PropertyType).GetValue(0));
        builder.AddAttribute(5, "ValueChanged", CreateCallback<Enum>(property));
        builder.AddAttribute(6, "ValueExpression", CreateExpression<Enum>(property));

        foreach (var value in Enum.GetValues(property.PropertyType))
        {
            builder.OpenElement(1, "option");
            builder.AddAttribute(2, "value", value.ToString());
            builder.CloseElement();
        }
        builder.CloseElement();
    }


    private void BuildComponent<PropertyType>(PropertyInfo property, RenderTreeBuilder builder, Type inputType)
    {
        var propertyValue = property.GetValue(Model);
        var id = Guid.NewGuid();
        builder.AddMarkupContent(0, $"<label for=\"{id}\">{property.Name}</label>");
        builder.OpenComponent(1, inputType);
        builder.AddAttribute(2, "id", id.ToString());
        builder.AddAttribute(3, "Value", propertyValue);
        builder.AddAttribute(5, "ValueChanged", CreateCallback<PropertyType>(property));
        builder.AddAttribute(6, "ValueExpression", CreateExpression<PropertyType>(property));
        builder.CloseComponent();
    }

    private EventCallback<PropertyType> CreateCallback<PropertyType>(PropertyInfo property)
    {
        return RuntimeHelpers.TypeCheck(EventCallback.Factory.Create(this, EventCallback.Factory.CreateInferred(this, __value => property.SetValue(Model, __value), (PropertyType)property.GetValue(Model))));
    }
  

    private Expression<Func<PropertyType>> CreateExpression<PropertyType>(PropertyInfo property)
    {
        var constant = Expression.Constant(Model, Model.GetType());
        var exp = Expression.Property(constant, property.Name);
        return Expression.Lambda<Func<PropertyType>>(exp);
    }

}

它在这一行崩溃:return Expression.Lambda<Func<PropertyType>>(exp); 并出现此错误:System.ArgumentException: 'Expression of type 'Backender.Core.Common.Enums.EntityFieldType' cannot be used for return type 'System.Enum''。 EntityFieldType 也是一个枚举。 有什么建议吗?

类型不匹配。在构建表达式时,您使用 Enum 类型,它是所有枚举的父类型。但是,当您的组件被使用时,它会收到一个特定的 Enum,该 EnumEnum 的后代。您应该创建特定于传递的枚举的表达式和回调,而不是一般的 Enum 类型。

一般来说,您不能将泛型参数替换为子类型。这只能在泛型参数是协变的时候完成,也就是说,当它被定义为类似...... Expression<TDelegate>.

不会发生

设法通过使用更多反射来实现此功能:

private void BuildEnumSelectComponent(PropertyInfo property, RenderTreeBuilder builder)
{
    // When the elementType that is rendered is a generic Set the propertyType as the generic type
    var elementType = typeof(InputSelectWithOptions<>);
        Type[] typeArgs = { property.PropertyType };
        elementType = elementType.MakeGenericType(typeArgs);
   
    // Activate the the Type so that the methods can be called
    var instance = Activator.CreateInstance(elementType);
    var Value = property.GetValue(Model);
    var id = Guid.NewGuid();
    builder.AddMarkupContent(0, $"<div><label for=\"{id}\">{property.Name}</label></div>");
    builder.OpenComponent(1, instance.GetType());
    builder.AddAttribute(2, "id", id.ToString());
    builder.AddAttribute(3, "Value", Value);


    var method = this.GetType().GetMethod("CreateCallback");
    method= method.MakeGenericMethod(typeArgs);
    var callback = method.Invoke(this, new object[] { property });

    builder.AddAttribute(4, "ValueChanged", callback);
    

    // Create an expression to set the ValueExpression-attribute.
    var constant = Expression.Constant(Model, Model.GetType());
    var exp = Expression.Property(constant, property.Name);
    var lamb = Expression.Lambda(exp);
    builder.AddAttribute(5, "ValueExpression", lamb);
    builder.AddAttribute(6, "ChildContent",
       new RenderFragment(builder =>
       {
                // when type is a enum present them as an <option> element 
                // by leveraging the component InputSelectOption
                var values = property.PropertyType.GetEnumValues();
               foreach (var val in values)
               {
                    //  Open the InputSelectOption component
                    builder.OpenComponent(0, typeof(InputSelectOption<string>));

                    // Set the value of the enum as a value and key parameter
                    builder.AddAttribute(1, nameof(InputSelectOption<string>.Value), val.ToString());
                   builder.AddAttribute(2, nameof(InputSelectOption<string>.Key), val.ToString());

                    // Close the component
                    builder.CloseComponent();
               }

       }));
    builder.CloseComponent();
}

前面代码的问题是我们不知道Enum的具体类型(就像Francesco说的)。但是我们可以使用反射来利用现有的 CreateCallback 方法,方法是在运行时定义泛型参数:

var method = this.GetType().GetMethod("CreateCallback");
method= method.MakeGenericMethod(typeArgs);
var callback = method.Invoke(this, new object[] { property });

现在很有魅力。