'How do I dynamically create Input forms in Blazor based on type of property?

I'm attempting to loop through a model and create a form component for each property in the model. I am continuing to encounter various errors and need some help. Here is the relevent code:

forms.razor

<div class="form-section">
  @foreach (var property in DataModel.GetType().GetProperties())
  {
    var propertyString = $"DataModel.{property.Name.ToString()}";

    @if (property.PropertyType == typeof(DateTime))
    {
      <InputDate id=@"property.Name" @bind-Value="@propertyString">
    }
    else if (property.PropertyType == typeof(int))
    {
      <InputNumber id=@"property.Name" @bind-Value="@propertyString">
    }
    else
    {
      <InputText id=@"property.Name" @bind-Value="@propertyString">
    }
  }
</div>

DataModel.cs

public class DataModel
{
  public DateTime SelectedData { get; set; }

  public int SelectedNumber { get; set; }

  public decimal SelectDecimal { get; set; }
}

Obviously, I've simplified the code greatly. The div in forms.razor shown above is nested in an EditForm element. This is looping through the properties of the class DataModel, and if the type is a DateTime, it should render an InputDate form, or an InputNumber form, etc. If there were only 3 properties I would just hard-code them, but in the actual application I have over 100 properties of either DateTime, int, or decimal types, so I'm looking for a more programmatic solution. My goal is to get this to check the type of each property in the class, then correctly render the appropriate form associate with that data type, bound to the correct property. Presently, I can only get the InputDate form to render, but when I enter a date in the form I get the following exception:

Error: System.InvalidOperationException: The type 'System.String' is not a supported date type

I'm guessing this is due to the fact that I'm passing in a string of the property name in the @bind-Value parameter of the InputDate component. I cannot figure out to pass the actual reference to the property though. Can anyone help me to hone in on how to resolve this issue?



Solution 1:[1]

I finally figured this one out. I ended up using a callback in the onchange attribute that set the value of the current property in the loop.

<div class="form-section">
  @foreach (var property in DataModel.GetType().GetProperties())
  {
    var propertyString = $"DataModel.{property.Name.ToString()}";

    @if (property.PropertyType == typeof(DateTime))
    {
      <input type="date" id="@property.Name" @onchange="@(e => property.SetValue(dataModel, DateTime.Parse(e.Value.ToString())))" />
    }
    else if (property.PropertyType == typeof(int))
    {
      <input type="number" id="@property.Name" @onchange="@(e => property.SetValue(dataModel, Int32.Parse(e.Value.ToString())))" />
    }
    else
    {
      <input type="number" id="@property.Name" @onchange="@(e => property.SetValue(dataModel, Decimal.Parse(e.Value.ToString())))" />
    }
  }
</div>

Solution 2:[2]

I think what @nocturns2 said is correct, you could try this code:

@if (property.PropertyType == typeof(DateTime))
{
  DateTime.TryParse(YourPropertyString, out var parsedValue);
  var resutl= (YourType)(object)parsedValue
  <InputDate id=@"property.Name" @bind-Value="@resutl">
}

Solution 3:[3]

Here's a componentized version with an example component for DateOnlyFields

@using System.Reflection
@using System.Globalization

<input type="date" value="@GetValue()" @onchange=this.OnChanged @attributes=UserAttributes />

@code {

    [Parameter] [EditorRequired] public object? Model { get; set; }
    [Parameter] [EditorRequired] public PropertyInfo? Property { get; set; }
    [Parameter] public EventCallback ValueChanged { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();

    private string GetValue()
    {
        var value = this.Property?.GetValue(Model);
        return value is null
            ? DateTime.Now.ToString("yyyy-MM-dd")
            : ((DateOnly)value).ToString("yyyy-MM-dd");
    }

    private void OnChanged(ChangeEventArgs e)
    {
        if (BindConverter.TryConvertToDateOnly(e.Value, CultureInfo.InvariantCulture, out DateOnly value))
            this.Property?.SetValue(this.Model, value);
        else
            this.Property?.SetValue(this.Model, DateOnly.MinValue);

        if (this.ValueChanged.HasDelegate)
            this.ValueChanged.InvokeAsync();
    }
}

And using it:

    @foreach (var property in dataModel.GetType().GetProperties())
    {
        @if (property.PropertyType == typeof(DateOnly))
        {
            <div class="form-text">
                <DateOnlyEditor class="form-control" Model=this.dataModel Property=property ValueChanged=this.DataChanged />
            </div>
        }
    }
<div class="p-3">
    Date: @dataModel.SelectedDate
</div>

@code {
    private DataModel dataModel = new DataModel();
    private EditContext? editContext;

    public class DataModel
    {
        public DateOnly SelectedDate { get; set; } = new DateOnly(2020, 12, 25);
        public int SelectedNumber { get; set; }
        public decimal SelectDecimal { get; set; }
    }

    protected override void OnInitialized()
    {
        this.editContext = new EditContext(dataModel);
        base.OnInitialized();
    }

    void DataChanged()
    {
        StateHasChanged();
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 saganaga
Solution 2 user13256346
Solution 3 MrC aka Shaun Curtis