Вопрос

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.

Другие советы

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

There is a default validation provider in MVC (not EF), checking two things :

  • the type of the provided value (a string in an int property) => (not sure, but something like) yyy is not valid for field xxx

  • a "check null" attribute for Value types (it will complain if you let an empty field corresponding to an int property, and would accept an empty field for an int? property). => The xxx field is required

This second behaviour can be desactivated in global.asax (the property name is rather clear) :

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

With client side validation enabled, these validations, and the one related to DataAnnotations (Required, StringLength...) will raise Validation errors on client side, before going to the Controller. It avoids a roundtrip on the server, so it's not useless. But of course, you can't rely on client validation only.

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?

Hmmm, I must admit I ain't got a satisfying answer... So I let this one for a more competent one. They are taken as error in the ModelState.IsValid, because when the ClientSide Validation passed, you then go to ModelBinding (Model binding looks at your POSTed values, looks at the arguments of the corresponding HttpPost method (ActionResult Create for you), and try to bind the POSTed values with these arguments. In your case, the binding sees a Activity activity argument. And he doesn't get anything for StageId (for example) in your POSTed fields. As StageId is not nullable, he puts that as an error in the ModelState dictionary => ModelState is not valid anymore.

But I don't know why it's not catched by Client side validation, even with a Required attribute.

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?

Well, you'd have to turn off client validation, as you can't trust client validation only. But client validation, as said, is fine to avoid a useless roundtrip to the server.

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

It's only if you ain't got a ViewModel, but work on your Model class. It's only usefull when you work with Model First or Database First, as your entity classes are generated (with T4) each time your edmx changes. Then, if you put custom data annotations on your class, you would have to put it back manually after each class (file) generation, which would be stupid. So the [MetadataType(typeof()]] is a way to add annotations on a class, even if the "base class files" are re-generated.

Hope this helps a bit.

By the way, if you're interested in validation, take a look on FluentValidation. This is a very nice... fluent validation (would you have guessed ?) library.

  1. If the field is not nullable, EF think that it is required.
  2. Cause foreign keys is not nullable, so navigation properties is required too.
  3. To get all validation at once you need to use ViewModel that is transformed in entity mode in controller after validation. More about attribute validation in mvc, you can read here.
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top