Form binding of complex model in .net 8 Blazor Webassembly

214 Views Asked by At

I have a complex model that I would like to post in a Form, but It doesn't get binded. Only first layer has data. Model.Items and subsequent item.Items doesn't get binded. Components are interacting ok, Removing and Adding items as expected.

all code is a bit shortened for easier read :) These are the models:

public class NewOrEditFactoryOrderVM
{
    public int FactoryOrderId { get; set; }

    public int? OrderId { get; set; } = null;

    public int No { get; set; }

    public DateTime DocDate { get; set; }

    public List<NewOrEditFactoryOrderItemVM>? Items = new List<NewOrEditFactoryOrderItemVM>();
}

public class NewOrEditFactoryOrderItemVM
{
    public int FactoryOrderItemId { get; set; }

    public int FactoryOrderId { get; set; }

    ...

    public List<NewOrEditFactoryItemComponentVM>? Items { get; set; } = new List<NewOrEditFactoryItemComponentVM>();
}

public class NewOrEditFactoryItemComponentVM
{
    public int FactoryItemComponentId { get; set; }

    public int FactoryOrderItemId { get; set; }

    public int ProductId { get; set; }

    ...
}

And I have Parent componet with a form:

<EditForm Model="Model" OnValidSubmit="Submit" FormName="NewOrEditFactoryOrder" Enhance>
    <div class="row">
        <div class="col-20">
            <DataAnnotationsValidator />
            <ValidationSummary />
        </div>

        <div class="clearfix"></div>

        <div class="col-md-2">
            <input type="hidden" @bind="@Model.FactoryOrderId" />
            <input type="hidden" @bind="@Model.OrderId" />
            <label for="WorkOrderNo" class="form-label">@Frontend.No<span class="text-danger"> *</span></label>
            <InputNumber class="form-control" @bind-Value="Model.No" />
            <ValidationMessage For="(() => Model.No)" />
        </div>
        <div class="col-md-3">
            <label for="DocDate" class="form-label">@Frontend.Date<span class="text-danger"> *</span></label>
            <InputDate class="form-control" @bind-Value="Model.DocDate" />
            <ValidationMessage For="(() => Model.DocDate)" />
        </div>

        <div class="clearfix"></div>

        <div class="col-md-3">
            <label class="form-label">@Frontend.SearchProduct <span class="text-danger">*</span></label>
            <input type="text" placeholder="@Frontend.SearchPlaceholder" class="form-control" @oninput=@SearchProduct />
            @if (productsDD != null)
            {
                <ul class="list-group">
                    @foreach (var item in productsDD)
                    {
                        if (item.Selected == true)
                        {
                            <li @onclick="() => SelectConnectedWarehouse(item.Id)" class="list-group-item" data-bs-toggle="modal" data-bs-target="#[email protected]">@item.Name</li>
                        }
                        else
                        {
                            <li @onclick="() => AddProduct(item.Id, null)" class="list-group-item">@item.Name</li>
                        }
                    }
                </ul>
            }
        </div>
        <div class="col-md-17">
            <CascadingValue Value="Model" Name="Model">
                <NOEFactoryOrderItemCom ></NOEFactoryOrderItemCom>
            </CascadingValue>
            
        </div>

        <div class="clearfix"></div>

        <div class="col text-end">
            <input type="submit" value="@Frontend.Save" class="btn btn-primary" />
        </div>
    </div>
</EditForm>

Inside a form I call another component to manage Model.Items:

<CascadingValue Value="Model" Name="Model">
    <NOEFactoryOrderItemCom ></NOEFactoryOrderItemCom>
</CascadingValue>

Its content:

@if (Model != null && Model.Items != null && Model.Items.Any())
{
    <hr />
    foreach (var item in Model.Items)
    {
        <div class="row">
            <input type="hidden" @bind="@item.FactoryOrderItemId" />
            <input type="hidden" @bind="@item.FactoryOrderId" />
            <input type="hidden" @bind="@item.OrderItemId" />

            <div class="col-md-4">
                <label for="Items.Description" class="form-label">
                    @item.ProductName
                    @if (item.ConnectedId != null && item.ConnectedId > 0)
                    {
                        <span> + </span>

                        @item.ConnectedName
                    }
                </label>
                @if (string.IsNullOrWhiteSpace(item.Comment))
                {
                    <InputTextArea class="form-control" @bind-Value="@item.Comment" rows="1" />
                }
                else
                {
                    <InputTextArea class="form-control" @bind-Value="@item.Comment" rows="4" />
                }
            </div>
            <div class="col-md-2">
                <label for="Items.Quantity" class="form-label gremlin-label">@Frontend.Quantity</label>
                <InputNumber class="form-control" @[email protected] TValue="decimal" />
            </div>
            <div class="col-sm-12"> </div>
            <div class="col-md-1">
                <button type="button" class="btn btn-danger" @onclick="@(() => RemoveItem(item))">
                    <i class="bi bi-x-circle-fill"></i>
                </button>
            </div>
        </div>
        <div class="row">
            <CascadingValue Name="Items" Value="item.Items">
                <NOEFactoryItemComponentCom ></NOEFactoryItemComponentCom>
            </CascadingValue>
        </div>
        <hr />
    }
}

@code {
    [Inject]
    private IFactoryServices _factory { get; set; } = null!;

    [CascadingParameter(Name = "Model")]
    public NewOrEditFactoryOrderVM? Model { get; set; }

And finally 2. child component:

@if (SubItems != null && SubItems.Any())
{
    foreach (var subItem in SubItems)
    {
        <div class="col-md-1 col-sm-1"> </div>

        <div class="col-md-4">
            <label class="form-label gremlin-label">@Frontend.Item</label>
            <br />
            @subItem.ProductName
        </div>
        <div class="col-md-2">
            <label for="Items.Quantity" class="form-label gremlin-label">@Frontend.Quantity</label>
            <InputNumber class="form-control" @[email protected] TValue="decimal" />
        </div>
        @if (subItem.IsService)
        {
            <div class="col-md-2">
                <label for="Items.Workers" class="form-label gremlin-label">@Frontend.Workers</label>
                <InputNumber class="form-control" @bind-Value="@subItem.Workers" TValue="int" />
            </div>
        }
        else
        {
            <input type="hidden" name="Items.Workers" value="1" />
            <div class="col-md-2"> </div>
        }
        @if (subItem.HasDimensions)
        {
            <div class="col-md-3">
                <label for="subItem.x" class="form-label gremlin-label">@Frontend.X</label>
                <InputNumber class="form-control" @bind-Value="@subItem.x" TValue="decimal?" />
            </div>
            <div class="col-md-3">
                <label for="subItem.y" class="form-label gremlin-label">@Frontend.Y</label>
                <InputNumber class="form-control" @bind-Value="@subItem.y" TValue="decimal?" />
            </div>
            <div class="col-md-3">
                <label for="subItem.z" class="form-label gremlin-label">@Frontend.Z</label>
                <InputNumber class="form-control" @bind-Value="@subItem.z" TValue="decimal?" />
            </div>
        }
        else
        {
            <div class="col-md-9"> </div>
        }
        <div class="col-md-1">
            <button type="button" class="btn btn-danger" @onclick="@(() => RemoveSubItem(subItem))">
                <i class="bi bi-x-circle-fill"></i>
            </button>
        </div>
        <div class="clearix"> </div>
    }
}
@code {
    [Inject]
    private IFactoryServices _factory { get; set; } = null!;

    [CascadingParameter(Name ="Items")]
    public List<NewOrEditFactoryItemComponentVM>? SubItems { get; set; }
}
1

There are 1 best solutions below

0
On BEST ANSWER

So. The problem in fact was not Form binding. It is hard to debug Blazor webassembly.

Problem was that data serialization was not performing as expected. Only solution that worked was using Newtonsoft.Json:

Sending part:

public async Task<NewOrEditFactoryOrderVM?> SaveFactoryOrder(NewOrEditFactoryOrderVM? vm)
{
    if(vm != null)
    {
        string json = JsonConvert.SerializeObject(vm);
        StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync("api/Factory/SaveFactoryOrder", content);
        if (response.IsSuccessStatusCode)
        { ... rest of logic

and on API side:

[HttpPost("SaveFactoryOrder")]
public async Task<ActionResult<NewOrEditFactoryOrderVM>> SaveFactoryOrder()
{
    try
    {
        string requestBody = await new StreamReader(Request.Body).ReadToEndAsync();
        NewOrEditFactoryOrderVM? vm = JsonConvert.DeserializeObject<NewOrEditFactoryOrderVM>(requestBody);
        ... rest of logic

I hope this helps somebody. I hours and a lot of frustration. Was not able to find root cause :(