Page Validation for Large Forms in MVC

Home » ASP.NET MVC » C# » Page Validation for Large Forms in MVC
ASP.NET MVC Logo

Summary

A recent application I worked on had a large form that consisted of over 10 tabs worth of information – over 100 fields in total. This information came from another source (a tablet), and needed to be fully validated before being processed further. I searched around on the web looking for an example of how to most efficiently do this, and didn’t find one. Sure the standard MVC unobtrusive validation could be used, but I wanted to improve on it, so that:

  1. The user would see validation errors when entering the page.
  2. They did not have to fill in every single field with a validation error in order to save, because the information may not be known at the time.

One very important angle is that there has to be two types of validation. The type of validation that must always be validated is for example entering a string into a numeric field, or a characters into a date field. For this, I used standard MVC unobtrusive validation and data annotations.

The only problem I can find with my solution is that client side validation has to be turned off. Its a total server side solution at present. I would be interested in receiving any feedback particularly in this regard as obviously client side validation is a good thing. For me in the app I was working on however, which is an intranet app with a couple of users, turning off client side validation is not a big deal. It might be if the system had hundreds of users.

How it Works

The core MVC validation was used to validate things like data types, required fields, max lengths, in fact anything that if not validated would cause an error when trying to save. I also looked at IValidatableObject. The problem I had was that the validation would do two passes through, it would look at things like Required fields and MaxLengths, then if everything checked out ok would then run the Validate() method on IValidatableObject as well as. I wanted to split this out so I had two tiers of validation – validation that would just come up on screen and not stop the save, and validation that stopped the save.

Getting Started

To start on a tangent, getting all those fields on one page was easy enough – as I was using Bootstrap 3 framework, Bootstrap tabs was a reasonably obvious solution.

I created the model classes, and put the necessary data annotations on these classes to prevent problems with the save, e.g. Max lengths, data types, and required just for fields that were defined as NOT NULL in the database.

The Second Level of Validation

I wanted to use as much of the MVC framework as possible, so the next step was to create an interface, which I called IBcValidatableObject, with one method:


    public interface IBcValidatableObject
    {
        IEnumerable DoValidate(ValidationContext validationContext);
    }

The implementation of this interface in the model is exactly the same as for IValidatableObject, its just that it has to be manually called, it isn’t called automatically by MVC. An example follows:


     public IEnumerable DoValidate(ValidationContext validationContext) {
       var results = new List();

       if (string.IsNullOrWhiteSpace(AssessorName))
       {
           results.Add(new ValidationResult("Assessor Name is mandatory", new [] { "AssessorName" }));
       }

       return results;
     }

The next step is to alter the controller to do the validation when entering the edit page. In this circumstance, ALL the validation needs to be executed, so all errors are displayed on the page:


     // GET: Forms/Edit/5
     public ActionResult Edit(int? id)
     {
         /* Standard MVC processing */
         if (id == null)
         {
             return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
         }

         var form = _service.GetEnrolFormById((int)id);
         if (form == null)
         {
             return HttpNotFound();
         }

         /* Perform Full validation of ALL fields */
         var errors = _service.FullValidation(enrolForm);

         /* Add any errors to the ModelState */
         AddErrorsToModelState(errors);

         /* Populate Drop down lists for the form (not shown) */
         DoDropDownLists();

         return View(form);
     }

AddErrorsToModelState was a function I created to translate the output from the validate method to the page, as follows:


     private void AddErrorsToModelState(IEnumerable errors)
     {
         foreach (var error in errors)
         {
             if (!error.MemberNames.Any())
             {
                 ModelState.AddModelError(string.Empty, error.ErrorMessage);
             }
             else
             {
                 foreach (var member in error.MemberNames)
                 {
                     ModelState.AddModelError(member, error.ErrorMessage);
                 }
             }
         }
     }

When the user pressed Save, the standard MVC validation has to take place to ensure the record can be saved, then additional validation takes place to ensure that the correct errors are displayed on the page:


     // POST: Forms/Edit/5
     [HttpPost]
     [ValidateAntiForgeryToken]
     public ActionResult Edit(int id)
     {
         /* Get the data from the service */
         var form = _service.GetEnrolFormById(id);

         /* Try to get the data from the model and update, if successful, save is ok to do */
         if (TryUpdateModel(form))
         {
             form.Id= id;
             _service.Save(form);
         }

         /* Get additional errors for display purposes, if there aren't any I'm returning */
         /* to the previous page, if there are, I'm displaying them */
         var errors = _service.AdditionalValidation(form).ToList();
         if (!errors.Any())
             return RedirectToAction("Index");

         AddErrorsToModelState(errors);
     
         DoEnrolFormDropDownLists();

         return View(enrolForm);
     }

Thats it! The only thing missing from this example is the code I used in the service to do the different validation I needed:

  1. Full Validation
  2. Additional Validation

Full Validation


     public IEnumerable FullValidation(Form form)
     {
         var validationResults = new List();
         validationResults.AddRange(ObjectValidation(form.Data).ToList());
         validationResults.AddRange(AdditionalValidation(enrolForm));
         return validationResults;
     }

     /* ObjectValidation calls the standard validation */
     private IEnumerable ObjectValidation(object data)
     {
         var context = new ValidationContext(data, serviceProvider: null, items: null);
         var results = new List();
         Validator.TryValidateObject(data, context, results);
         return results;
     }  

Additional Validation


     public IEnumerable AdditionalValidation(Form form)
     {
         var validationResults = new List();
         var bcObject = form.Data as IBcValidatableObject;
         if (bcObject != null)
         {
             var validationContext = new ValidationContext(bcObject, serviceProvider: null, items: null);
             validationResults.AddRange(bcObject.DoValidate(validationContext));
         }

         return validationResults;
     }

N.B. In this example for clarity, I have removed the code that handles multiple sections in my model.

As always, any and all feedback welcome.

About Phil

I have been working as a software developer since 1983. This blog could have been called "From Fortran 77, C and Cobol to C# in 20 (not so) easy years", but it doesn't sound quite right somehow. Besides I'm talking about what's happened since 2003, not before!

Leave a Reply

Your email address will not be published.


You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>




Top Posts & Pages

Categories

Recent Posts

Recent Comments

Archives

Blogroll

  • Mike Cohn's Blog
  • Scott Hanselman's Blog
Be a Great Product Owner: Six Things Teams and Scrum Masters Need

Learn six ways effective product owners ensure their teams’ success. [...]

What Happens When During a Sprint

Succeeding with Scrum is easier when you know when and why to conduct each of the Scrum events during the sprint. [...]

What Are Agile Story Points?

Story points are perhaps the most misunderstood topic in agile. Story points are not based on just one factor--such as complexity, as is often mistakenly claimed. Instead, story points are based on a combination of factors. [...]

Don’t Equate Story Points to Hours

I’ve been quite adamant lately that story points are about time, specifically effort. But that does not mean you should say something like, “One story point = eight hours.” Doing this obviates the main reason to use story points in the... [...]

Epics, Features and User Stories

I've been getting more and more emails lately from people confused about the difference between "user stories", "epics" and "features." So I thought this month we'd return and cover some basic--but very helpful--territory by explaining those terms. First, the terms don't matter that much. These are not terms with important specific meanings like "pointer" to a programmer or "collateralized debt obligation" to whomever it is that's important. [...]

- Scott Hanselman
Use your own user @ domain for Mastodon discoverability with the WebFinger Protocol without hosting a server

Mastodon is a free, open-source social networking service that is decentralized and distributed. It was created in 2016 as an alternative to centralized social media platforms such as Twitter and Facebook. One of the key features of Mastodon is the use of the WebFinger protocol, which allows users to discover and access information about other users on the Mastodon network. WebFinger is a simple HTTP-based protocol that enables a user to discover information about other users or resources on the internet by using their email address or other identifying information. The WebFinger protocol is important for Mastodon because it enables… [...]

- Scott Hanselman
I got tired

I have been blogging here for the last 20 years. Every Tuesday and Thursday, quite consistently, for two decades. But last year, without planning it, I got tired and stopped. Not sure why. It didn't correspond with any life events. Nothing interesting or notable happened. I just stopped. I did find joy on TikTok and amassed a small group of like-minded followers there. I enjoy my YouTube as well, and my weekly podcast is going strong with nearly 900 (!) episodes of interviews with cool people. I've also recently started posting on Mastodon (a fediverse (federated universe)) Twitter alternative that… [...]

- Scott Hanselman
Using Home Assistant to integrate a Unifi Protect G4 Doorbell and Amazon Alexa to announce visitors

I am not a Home Assistant expert, but it's clearly a massive and powerful ecosystem. I've interviewed the creator of Home Assistant on my podcast and I encourage you to check out that chat. Home Assistant can quickly become a hobby that overwhelms you. Every object (entity) in your house that is even remotely connected can become programmable. Everything. Even people! You can declare that any name:value pair that (for example) your phone can expose can be consumable by Home Assistant. Questions like "is Scott home" or "what's Scott's phone battery" can be associated with Scott the Entity in the… [...]

- Scott Hanselman
JavaScript and TypeScript Projects with React, Angular, or Vue in Visual Studio 2022 with or without .NET

I was reading Gabby's blog post about the new TypeScript/JavaScript project experience in Visual Studio 2022. You should read the docs on JavaScript and TypeScript in Visual Studio 2022. If you're used to ASP.NET apps when you think about apps that are JavaScript heavy, "front end apps" or TypeScript focused, it can be confusing as to "where does .NET fit in?" You need to consider the responsibilities of your various projects or subsystems and the multiple totally valid ways you can build a web site or web app. Let's consider just a few: An ASP.NET Web app that renders HTML… [...]

- Scott Hanselman
A Nightscout Segment for OhMyPosh shows my realtime Blood Sugar readings in my Git Prompt

I've talked about how I love a nice pretty prompt in my Windows Terminal and made videos showing in detail how to do it. I've also worked with my buddy TooTallNate to put my real-time blood sugar into a bash or PowerShell prompt, but this was back in 2017. Now that I'm "Team OhMyPosh" I have been meaning to write a Nightscout "segment" for my prompt. Nightscout is an open source self-hosted (there are commercial hosts also like T1Pal) website and API for remote display of real-time and near-real-time glucose readings for Diabetics like myself. Since my body has an… [...]

Meta