Feeds:
Posts
Comments

Archive for July, 2012

In a recent project, one of my models had the following validation requirements:
If “CompletedDate” has a value (a date value), “Status” must be set to “Completed”. If “Status” is set to “Completed”, the “CompletedDate” must be set.

In other words, validation wasn’t simply that a value must be a string, or a number between x and y, etc; the validation of one field is dependant on the value in the other and vice versa.

I first went down this route, running the validation in the data layer (in the Entity Framework stuff):
MSDN: How to: Customize Data Field Validation in the Data Model

This was pretty good, but the problem is *when* the validation occurs when using this approach: when the model is being saved. I want validation to run *before* saving the data, so that MVC can know that there’s a problem, and send some friendly message to the user. A typical “edit” command in a controller looks like this:

[HttpPost]
public ActionResult Edit(ProjectTask projecttask)
{
	if (ModelState.IsValid)
	{
		db.ProjectTasks.Attach(projecttask);
		db.ObjectStateManager.ChangeObjectState(projecttask, EntityState.Modified);
		db.SaveChanges();
		return RedirectToAction("Details", "Project", new { id = projecttask.Workorder.CallID });
	}
	return View(projecttask);
}

So I *do* have an option, I could put a try around db.SaveChanges, and catch my validation error there. However, I would then have to grab the error message and figure out some way to send that back to the client (via ViewBag, most likely). I didn’t like this.

I then inspected this ModelState.IsValid method, and had the thought to create an extension method along the lines of ModelState.IsValidProjectTask(). With this, I can add error messages into the ModelState, specific to the fields. This means Razor script like:

@Html.ValidationMessageFor(model => model.CompletionDate)

will work, returning a proper error message where appropriate.

So here’s what I did:

[HttpPost]
public ActionResult Edit(ProjectTask projecttask)
{
	if (ModelState.IsValidProjectTask())
	{
		db.ProjectTasks.Attach(projecttask);
		db.ObjectStateManager.ChangeObjectState(projecttask, EntityState.Modified);
		db.SaveChanges();
		return RedirectToAction("Details", "Project", new { id = projecttask.Workorder.CallID });
	}
	return View(projecttask);
}

and then in my Models namespace:

using System.Web.Mvc;

namespace MyProject.Models
{
    public static class ModelStateDictionaryExtensions
    {
        public static bool IsValidProjectTask(this ModelStateDictionary modelStateDictionary)
        {
            return modelStateDictionary.IsValid & ValidateDates(modelStateDictionary);
        }

        private static bool ValidateDates(ModelStateDictionary modelStateDictionary)
        {
            ModelState outState;
            var submittedDate = string.Empty;
            var submittedStatus = string.Empty;

            if (modelStateDictionary.ContainsKey("CompletionDate"))
            {
                modelStateDictionary.TryGetValue("CompletionDate", out outState);
                submittedDate = outState.Value.AttemptedValue;
            }
            if (modelStateDictionary.ContainsKey("Status"))
            {
                modelStateDictionary.TryGetValue("Status", out outState);
                submittedStatus = outState.Value.AttemptedValue;
            }

            if (submittedStatus.ToUpper() == "COMPLETED" & string.IsNullOrEmpty(submittedDate))
            {
                modelStateDictionary.AddModelError("CompletionDate", "Task completed, but no completion date provided.");
                return false;
            }
            if (submittedStatus.ToUpper() != "COMPLETED" & !string.IsNullOrEmpty(submittedDate))
            {
                modelStateDictionary.AddModelError("Status", "Completion date provided, but task not completed.");
                return false;
            }

            return true;
        }
    }
}
Advertisements

Read Full Post »