2

THE PROBLEM

I have a client view model with some required properties and non-required properties. The view contains different sections for updating different view model properties. If i am updating a part of the view with required client details such as FirstName, LastName, DOB, etc then i can wrap the input types into an ajax form and the controller will pick up these properties from the view model and validate accordingly using ModelState.IsValid and validation will be successful. However if I have another section on the same view that needs to update a non required property on the viewmodel (ie Notes) and pass this through from an ajax form post then ModelState validation fails because the other required properties are null as they were never submitted as part of the ajax form. Note that the required fields should always have data populated prior to loading the client details page and therefore should never be null.

THE CODE

ViewModel

public class ClientDetailViewModel
{

    public int ID { get; set; }

    [Required]
    [StringLength(50, MinimumLength = 2)]
    public string FirstName { get; set; }

    [Required]
    [StringLength(50, MinimumLength = 2)]
    public string LastName { get; set; }

    [Required]
    [Display(Name = "Date of Birth")]
    [DataType(DataType.Date)]
    public DateTime DOB { get; set; }

    [Required]
    public string Gender { get; set; }

    public string Notes { get; set; }
}

View

@model MSIC.Models.ClientViewModels.ClientDetailViewModel
@inject MSIC.Services.Custom.IGenderService GenderService;

<!-- tab-pane for updating core client details -->
<div class="tab-pane active" id="tab_1">
    <form asp-controller="Client" asp-action="Edit" class="form-horizontal">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="FirstName" class="col-sm-2 control-label"></label>
            <div class="col-sm-10">
                <input asp-for="FirstName" class="form-control" />
                <span asp-validation-for="FirstName" class="text-danger" />
            </div>
        </div>
        <div class="form-group">
            <label asp-for="LastName" class="col-sm-2 control-label"></label>
            <div class="col-sm-10">
                <input asp-for="LastName" class="form-control" />
                <span asp-validation-for="LastName" class="text-danger" />
            </div>
        </div>
        <div class="form-group">
            <label asp-for="DOB" class="col-sm-2 control-label"></label>
            <div class="col-sm-10">
                <input asp-for="DOB" class="form-control" />
                <span asp-validation-for="DOB" class="text-danger" />
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Gender" class="col-sm-2 control-label"></label>
            <div class="col-sm-10">
                <select asp-for="Gender" asp-items="@(new SelectList(GenderService.GetAll(),"Code","Name"))" class="form-control">
                </select>
                <span asp-validation-for="Gender" class="text-danger" />
            </div>
        </div>
    </form>
</div>

<!-- different section for updating client notes -->
<div id="divNotes" class="center-block">@Model.Notes</div>
<a href="#" class="btn btn-danger btn-block" data-toggle="modal" data-target="#notesModal" role="button"><b>Edit Notes</b></a>
<form asp-controller="Client" asp-action="EditNotes" class="form-horizontal" data-ajax="true" data-ajax-method="POST" data-ajax-update="#divNotes" data-ajax-mode="replace" data-ajax-success="CloseModal('#notesModal')" data-ajax-failure="AjaxOnFailure(xhr, status, error)">
    <div class="modal fade" id="notesModal" tabindex="-1" role="dialog" aria-labelledby="notesModalLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
                    <h4 class="modal-title" id="notesModalLabel">edit Reason</h4>
                </div>
                <div class="modal-body">
                    <input type="hidden" asp-for="ID" />
                    <div class="form-group">
                        <div class="col-sm-10">
                            <textarea asp-for="Notes" class="form-control" autofocus></textarea>
                            <span asp-validation-for="Notes" class="text-danger" />
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <div class="text-danger pull-left">
                        <i id="modalErrorIcon" class=""></i>
                        <span id="modalErrorText"></span>
                    </div>
                    <button type="submit" class="btn btn-primary">Save</button>
                    <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
                </div>
            </div>
        </div>
    </div>
</form>

Controller

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult EditNotes(ClientDetailViewModel model)
{
    //validation fails because required fields in model are null since they were not submitted with this ajax form
    if (ModelState.IsValid)
    {
        //update database with client notes and return notes to screen
        return Content(model.Notes);
    }

    return Content("i haven't coded this yet");
}

THE QUESTIONS

  1. How can I perform ModelState validation inside the controller for only some fields in an efficient way without duplicating code? I searched this issue and am aware of options like ModelState[].Errors.Clear(); but since this will be a big view with many different sections available for update I would like to avoid duplicating those statements in different Action methods for all the small ajax posts I will need to perform. Basically I would like to use ModelState validation so i can just store my validation logic inside the ViewModel and not separate and/or duplicate any validation logic inside the controller too.

  2. The other option I can see would be to include all the required properties as hidden input types in each ajax form i have but this seems horribly unecessary and would surely be a nightmare to maintain. Is there a better way to pass all ViewModel properties for an ajax post and if so would it be expensive to send ViewModel properties that are not used other than for taking advantage of the ModelState.IsValid check?

  3. I am new to asp.net and have started with asp.net core mvc (which i'm enjoying a lot) and have been learning via all the tutorials, SO questions, etc. However is it possible that I am approaching this the wrong way and if so what would be the right way to tackle this issue using asp.net core and Microsoft.jQuery.Unobtrusive.Ajax or some other ajax tool? Note that I have only posted this question because none of the other similar issues posted seem to deal with sharing ViewModel properties on a View but only submitting some of the properties but taking advantage of out-of-the-box validation.

Thanks in advance.

2 Answers 2

1

Although I could not get the recommended approach to behave as suggested I did use the advice given about separating out the view models for rendering and submitting. I think I was led astray by the Movie tutorial on the Microsoft ASP.NET Core site which uses the same Movie model for both the detail and edit pages.

To resolve my issue going forward I have adopted a large view model for the detail page and I will use View Components for the various parts of my page that need updating. This allows me to have a separate View Model for each View Component and my controller post action can accept just the View Model that relates to the corresponding View Component so it can perform only the necessary ModelState validation.

This server side validation from a jQuery ajax post has led me to the next problem about returning the ModelState errors to the client for display but I have submitted a new question for that...

Returning BadRequest in ASP.Net Core MVC to Microsoft jQuery Unobtrusive Ajax post has ModelState undefined

Sign up to request clarification or add additional context in comments.

Comments

0

It's not expensive to send everything back for all calls (by using hidden fields), but it's hacky and not a direct solution.

The view models in MVC don't have to be the same class for rendering the page and submitting forms. In your case you could define a viewmodel for rendering like this:

public class ClientDetailViewModel {
    public ClientBasicViewModel Basics {get;set;}
    public ClientNotesViewModel Notes {get;set;}
}

Then put the fields (name, gender, etc) in each of the two new classes. Use two separate controller methods to handle form submissions. Tracing how FirstName gets sent and received:

  • When rendering, you would use Model.Basics.FirstName to show the existing value.
  • For the field name in the form, make it input name="FirstName"...
  • When submitting the form back, the controller method accepts an argument of type ClientBasicViewModel.

2 Comments

That's an interesting concept. I tried breaking the view model into separate sub models as you suggested and then on my controller i passed in one of the sub models instead of the parent viewmodel but the property values were null
The debugging can be a pain. Check the page source in the browser to ensure that the form field names (the name property, not the id property) is what is expected by the controller method that processes the form submission.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.