aevitas
aevitas

Reputation: 3833

How to properly preserve view rendering data on model validation failure?

In my application, I have a form where a user can add a bank account to their account. I've implemented this through two actions on my controller, namely:

public async Task<IActionResult> Add()
{
    var client = await _clientFactory.CreateClientAsync();

    // We obtain the available account types from a remote service.
    var accountTypes = await client.GetAccountTypesAsync();

    var viewModel = new AddAccountViewModel
    {
        AccountTypes = accountTypes;
    }

    return View(viewModel);
}

[HttpPost]
public async Task<IActionResult> Add(AddAccountViewModel model)
{
    if (!ModelState.IsValid)
        return View(model);

    // Store the account for the user, and redirect to a result page.
}

My AddAccountModel currently looks like this, and has two purposes:

public class AddAccountViewModel
{
    public List<AccountTypeModel> AccountTypes { get; set; }

    [Required]
    [StringLength(100)]
    public string AccountName { get; set; }

    [Required]
    [Range(1, int.MaxValue]
    public decimal CurrentBalance { get; set; }

    [Required]
    public Guid AccountTypeId { get; set; }
}

One the one hand, the AccountTypes property is used to populate a list in the view:

@{
    var accountTypeList = new SelectList(accountTypes, "Id", "Name");
}

<select class="form-control" id="accountType" asp-for="AccountTypeId" asp-items="@accountTypeList">

</select>

This all works fine when I arrive on the initial page via GET /Add. The form renders, the select box is populated with the items from the service, as I'd expect.

However, the form also includes validation, and should a user input invalid data or leave fields empty, POST /Add is called, passed the model, runs validation, and ultimately redirects the user back to GET /Add, with the specific reasons as to why validation failed. This is where the problem arises.

When the user is redirected back to the form and asked to correct their input, AccountTypes in my model is null. Which makes sense, as we didn't send it as a part of the POST request - it only serves as input for the form to render. With the form not knowing the account types, the user will never be able to correct their input, as they won't be able to select a valid account type.

As mentioned above, my model is two-fold: on the one hand it contains data the view requires to render, and on the other hand it contains data the user provides and is then processed by the application.

Given the above, my question is: how should I properly preserve the input data required for my view to render when model validation failed? I.e., is my ViewModel the correct place to keep the collection of AccountTypeModels, or should I be keeping them in ViewData, or perhaps even TempData?

Currently, both my application and services are stateless, which is why I didn't go down the "just stick it in TempData" route yet, as from what I can gather, TempData is stored in the server's session. I also don't want to query the service for the available account types at the start of the view if I can avoid it, as it seems to violate MVC's separation of concerns.

Thanks for reading, and I hope someone can help me out with this.

Upvotes: 2

Views: 111

Answers (1)

user3559349
user3559349

Reputation:

You need to repopulate the AccountTypes collection in the POST method before you return the view. I would recommend that your view model contain a property public IEnumerable<SelectListItem> AccountTypes { get; set; } rather than List<AccountTypeModel> so that you do not need to generate the SelectList in the view.

To keep it DRY, create a private method to configure your view model

public async Task<IActionResult> Add()
{
    var model = new AddAccountViewModel();
    ConfigureViewModel(model);
    return View(model);
}

[HttpPost]
public async Task<IActionResult> Add(AddAccountViewModel model)
{
    if (!ModelState.IsValid)
    {
        ConfigureViewModel(model);
        return View(model);
    }
    ....
}

private async Task ConfigureViewModel(AddAccountViewModel model)
{
    var client = await _clientFactory.CreateClientAsync();
    var accountTypes = await client.GetAccountTypesAsync();
    model.AccountTypes = new SelectList(accountTypes, "Id", "Name");
}

Upvotes: 2

Related Questions