Building AgiliTrain: Part 3 - Data Validation


AgiliTrain Logo

In Part 1 and Part 2 of this series, I talked about why I used MVC to create my new venture and how I implemented MVC.  In this third part, I will talk about data entry and validation.

When I talk to a lot of people about MVC they point to jQuery and other frameworks as the reason they want to use MVC.  Certainly ASP.NET MVC allows us to program our views much closer to the HTTP protocol and the HTML DOM which means that scripting frameworks are easier. This means that you can do some amazing things with these tools in ASP.NET MVC without having to fit a square peg in a round hole. 

But for me I decided to not do much AJAX at all. This site has two goals: communicating information about classes and collection data for registration.  Lots of AJAX would look cool but isn't necessary. In my case I simply decided to do all server-side data validation. The only place I used jQuery at all was to do some quick pane switching on one of the entry screens.

In Part 2 I explained that I created Model classes that represented the data that was going down to the Views. I decided to use those models to handle the validation. In the Views you can use ValidationMessage objects to show messages when validation fails.  For example, a shortened version of one of my instructor data entry pages looks like this:

<% using (Html.BeginForm())
   { %>
 <div>Your Name</div>  
 <div><%= Html.TextBox("name") %>
      <%= Html.ValidationMessage("name") %></div>
 ...
 <div><%= Html.SubmitButton("submit", "Save") %></div>
<% } %>

You can see that I am showing a TextBox then a ValidationMessage right after it.  The idea here is that when the form is posted, if it fails validation, we'll return to it and show any validation errors directly in this form.

One common pattern I saw in some examples was to use to do the validation on the server in the Controller:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult ConfirmAccount(string username, 
                                   string password, 
                                   string id)
{
  // Basic parameter validation
  if (String.IsNullOrEmpty(username))
  {
    ModelState.AddModelError("username", 
                               "You must specify a username.");
  }
  if (String.IsNullOrEmpty(password))
  {
    ModelState.AddModelError("password", 
                               "You must specify a password.");
  }
  
  if (ViewData.ModelState.IsValid)
  {
    //...
  }

  // If we get this far, display the form with errors
  return View()
}

In this pattern the ModelState can be used to add errors to be displayed then check to see if the ModelState is valid before proceeding. I didn't like this style as it puts too much validation in the Controller. I sensed that if I had a model, the model should be responsible for validation. I would have preferred to have a loose coupling between the model and controller, but for brevity I sacrificed perfection and added a method to the Models called ValidateModel that took the ModelState:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Register(RegisterPostData data)
{
  data.ValidateModel(ViewData.ModelState);

  if (ViewData.ModelState.IsValid)
  {
    // ...
  }

  // If we get this far, display the form with errors
  return View()
}

I do the same work I would do in the Controller method but this style lets me encapsulate it to validate it anywhere I use the model.  For example:

public class RegisterPostData
{
  public void ValidateModel(ModelStateDictionary state)
  {
    RequiredField(state, this.FirstName, 
                  "firstname", "First Name");
    RequiredField(state, this.LastName, 
                  "lastname", "Last Name");
    RequiredField(state, this.Address1, 
                  "address1", "Address");
    RequiredField(state, this.CityTown, 
                  "citytown", "City/Town");
    RequiredField(state, this.StateProvince, 
                  "stateprovince", "State/Province");
    RequiredField(state, this.PostalCode, 
                  "postalcode", "PostalCode");
    RequiredField(state, this.Country, 
                  "country", "Country");
  }

  protected void RequiredField(ModelStateDictionary state, 
                               string data, 
                               string name, 
                               string desc)
  {
    if (String.IsNullOrEmpty(data))
    {
      state.AddModelError(name, 
                          string.Format("You must specify a {0}.", desc));
    }
  }
  // ...
}

Validating the Model is not necessarily this simple, it also might involve more complex validation.  For example, in my RegistrationModel I have to validate that either a valid a Credit Card number or Corporate PO information is supplied.  Your validation may be as simple or as complex as you require.  One small hint that is useful is that sometimes you have validation that is more about a genreal validation error (e.g. not validation on a single field). In that case when you call AddModelError you can specify "_FORM" instead of a field name and it will show up as a form-level error (using the ValidationSummary construct).

Again, I couild do double duty and have the validation performed at both the client and server and it would be slicker but with my short time-frame I decided to implement the important side first. I will likely revisit the pages and add client-side validation at some point.  Luckily MVC makes these sorts of refactoring especially simple since the View is so separated from the rest of the logic.



Shawn
Shawn Wildermuth
Author, Teacher, and Coach




My Courses

Wilder Minds Training
Vue.js by Example (New Lower Price)
Bootstrap 4 by Example (New Lower Price)
Intro to Font Awesome 5 (Free Course)
Pluralsight
Building an API with ASP.NET Core (New Course)
Building a Web App with ASP.NET Core, MVC6, EF Core, Bootstrap and Angular (updated for 2.2)
Less: Getting Started (New)
Using Visual Studio Code for ASP.NET Core Projects
Implementing ASP.NET Web API

Application Name WilderBlog Environment Name Production
Application Ver v4.0.30319 Runtime Framework x86
App Path D:\home\site\wwwroot\ Runtime Version .NET Core 4.6.27617.04
Operating System Microsoft Windows 10.0.14393 Runtime Arch X86