Reputation: 258
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
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
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