Ricky
Ricky

Reputation: 258

How can I dynamically build an Enum select with Blazor?

I'm trying to build a Generic form component with Blazor. Currently, all other input types are working except for enums selects. I think this happens because the compiler doesn't know the specific enum type when trying to add the expression and callback functions:

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

}

Its crashing in this line: return Expression.Lambda<Func<PropertyType>>(exp); with this error: System.ArgumentException: 'Expression of type 'Backender.Core.Common.Enums.EntityFieldType' cannot be used for return type 'System.Enum'' . EntityFieldType is also an enum. Any tips?

Upvotes: 0

Views: 1616

Answers (2)

Ricky
Ricky

Reputation: 258

Managed to get this working by using more reflection:

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

The problem with the previous code is that we don't know the specific type of Enum (like Francesco said). But we can use reflection to leverage the existing CreateCallback method, by defining the generic parameter at runtime:

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

Works like a charm now.

Upvotes: 0

Francesco Abbruzzese
Francesco Abbruzzese

Reputation: 4149

There is a type-mismatch. When building the expression you use the Enum type that is a parent type for all enums. However, when your component is used it receives a specific Enum that descends from Enum. You should create expressions and callbacks that are specific for the passed enum, not for the general Enum type.

In general, you can't replace generic arguments with subtypes. This can be done ONLY when the generic argument is covariant, that is, when it is defined with something like ..... This is the case for IEnumerable<T> that is defined as IEnumerable<out T>, but doesn't happen for Expression<TDelegate>.

Upvotes: 1

Related Questions