Fundamentals of Validation MVC (with Entity Framework)

advertisements

MVC Validation Fundamentals (with Entity Framework)

Scenario:

I have a model class as below (autogenerated via Entity Framework EF.x DbContext Generator).

(There is no view model at the moment).

public partial class Activity
{
    public int Id { get; set; }

    public byte Progress { get; set; }
    public decimal ValueInContractCurrency { get; set; }
    public System.DateTime ForecastStart { get; set; }
    public System.DateTime ForecastEnd { get; set; }

    public int DepartmentId { get; set; }
    public int OwnerId { get; set; }
    public int StageId { get; set; }
    public int StatusId { get; set; }

    public virtual Department Department { get; set; }
    public virtual Owner Owner { get; set; }
    public virtual Stage Stage { get; set; }
    public virtual Status Status { get; set; }
}

When I submit a blank form on the strongly-typed view, I get these validation messages:

The Progress field is required.

The ValueInContractCurrency field is required.

The ForecastStart field is required.

The ForecastEnd field is required.

i.e. all the fields in the db table.

If I fill these in, and submit again, then the controller gets called. The controller then returns back to the view page due to IsValid being false.

The screen is then redisplayed with these validation messages:

The StageId field is required.

The DepartmentId field is required.

The StatusId field is required.

The OwnerId field is required.

i.e. all the foreign key fields in the db table (these are also all select boxes).

If I fill these in, the form then submits succesfully and is saved to the db.

Questions:

  1. Where is the validation coming from, given that I have not used any [Required] attributes? Is this something to do with entity framework?

  2. Why is the form not validating everything right away client-side, what's different about foreign keys (or select boxes) that they are only checked by IsValid() even though they are empty and therefore clearly invalid?

  3. How do you make everything get validated in one step (for empty fields), so the user does not have to submit the form twice and all validation messages are shown at once? Do you have to turn off client side validation?

(I tried adding [Required] attributes to the foreign key fields, but that didn't seem to make any difference (presumably they only affect IsValid). I also tried calling Html.EnableClientValidation() but that didn't make any difference either).

4..Lastly, I've seen people using [MetadataType[MetadataType(typeof(...)]] for validation. Why would you do that if you have a viewmodel, or is it only if you don't?

Obviously I'm missing some fundamentals here, so in addition if anyone knows of a detailed tutorial on how exactly the MVC validation process works step-by-step including javascript/controller calls, rather than just another essay on attributes, then I could do with a link to that too :c)


More info for Mystere Man:

Solution setup as follows:

.NET4

MVC3

EF5

EF5.x Db Context Generator

"Add Code Generation Item" used on edmx design surface to associate EF.x Db Context Generator files (.tt files)

Controller looks like this:

    // GET: /Activities/Create
    public ActionResult Create()
    {
        ViewBag.DepartmentId = new SelectList(db.Departments, "Id", "Name");
        ViewBag.OwnerId = new SelectList(db.Owners, "Id", "ShortName");
        ViewBag.ContractId = new SelectList(db.Contracts, "Id", "Number");
        ViewBag.StageId = new SelectList(new List<string>());
        ViewBag.StatusId = new SelectList(db.Status.Where(s => s.IsDefaultForNewActivity == true), "Id", "Name");
        return View();
    } 

    // POST: /Activities/Create
    [HttpPost]
    public ActionResult Create(Activity activity)
    {
        if (ModelState.IsValid)
        {
            db.Activities.Add(activity);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        ViewBag.DepartmentId = new SelectList(db.Departments, "Id", "Name");
        ViewBag.OwnerId = new SelectList(db.Owners, "Id", "ShortName");
        ViewBag.ContractId = new SelectList(db.Contracts, "Id", "Number");
        ViewBag.StageId = new SelectList(db.Stages, "Id", "Number");
        ViewBag.StatusId = new SelectList(db.Status, "Id", "Name");
        return View(activity);
    }

View is like this:

<!-- this refers to  the EF.x DB Context class shown at the top of this post -->
@model RDMS.Activity  

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Activity</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.StageId, "Stage")
        </div>
        <div class="editor-field">
            @Html.DropDownList("StageId", String.Empty)
            @Html.ValidationMessageFor(model => model.StageId)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Progress)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Progress)
            @Html.ValidationMessageFor(model => model.Progress)
        </div>

    <!-- ETC...-->

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}


The reason why you get required validation is because the properties are value types (ie they can't be null). Since they can't be null, the framework requires you fill in values for them (otherwise it would have to throw some weird exception).

This problem manifests itself in several ways. I've seen this over and over and over here on Slashdot. I am not sure why so many people fall into this problem, but it's pretty common. Usually this results in a strange exception referring to no default constructor being thrown, but for some reason that did not happen here.

The problem stems from your use of ViewBag and naming the items in ViewBag the same as your model properties. When the page is submitted, the model binder gets confused by similarly named items.

Change these to add List at the end:

ViewBag.DepartmentList = new SelectList(db.Departments, "Id", "Name");
ViewBag.OwnerList = new SelectList(db.Owners, "Id", "ShortName");
ViewBag.ContractList = new SelectList(db.Contracts, "Id", "Number");
ViewBag.StageList = new SelectList(new List<string>());
ViewBag.StatusList = new SelectList(db.Status
        .Where(s => s.IsDefaultForNewActivity == true), "Id", "Name");

And change your view to use the strongly typed versions of DropDownListFor:

@Html.DropDownList(x => x.StageId, ViewBag.StageList, string.Empty)
... and so on

One other item of note. In the example above, I hope you're not using some kind of global data context or worse, a singleton. That would be disastrous and could cause data corruption.

If db is just a member of your controller that you new up in the constructor, that's ok, though not ideal. A better approach is to either create a new context in each action method, wrapped by a using statement (then the connection gets closed and destroyed right away) or implement IDisposable on the controller and call Dispose explicitly.

An even better approach is not doing any of this in your controller, but rather in a business layer, but that can wait until you're further along.