2

I'm working on a MVC5 Code-First application.

On one Model's Edit() view I have included [Create] buttons to add new values to other models from within the Edit() view and then repopulate the new value within DropDownFors() on the Edit().

For this first attempt, I am passing a model_description via AJAX to my controller method createNewModel():

[HttpPost]
public JsonResult createNewModel(INV_Models model)
{
    // model.model_description is passed in via AJAX -- Ex. 411

    model.created_date = DateTime.Now;
    model.created_by = System.Environment.UserName;
    model.modified_date = DateTime.Now;
    model.modified_by = System.Environment.UserName;

    // Set ID
    int lastModelId = db.INV_Models.Max(mdl => mdl.Id);
    model.Id = lastModelId+1;

    //if (ModelState.IsValid == false && model.Id > 0)
    //{
    //    ModelState.Clear();
    //}

    // Attempt to RE-Validate [model], still comes back "Invalid"
    TryValidateModel(model);

    // Store all errors relating to the ModelState.
    var allErrors = ModelState.Values.SelectMany(x => x.Errors);

    // I set a watch on [allErrors] and by drilling down into
    // [allErrors]=>[Results View]=>[0]=>[ErrorMessage] I get
    // "The created_by filed is required", which I'm setting....?

    try
    {
        if (ModelState.IsValid)
        {
            db.INV_Models.Add(model);
            db.SaveChangesAsync();
        }

    }
    catch (Exception ex)
    {
        Elmah.ErrorSignal.FromCurrentContext().Raise(ex);
    }

    return Json(
        new { ID = model.Id, Text = model.model_description },
        JsonRequestBehavior.AllowGet);
}

What I cannot figure out is why my ModelState is coming up as Invalid?

All properties are being specified before the ModelState check; the Model is defined as follows:

public class INV_Models
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Please enter a Model Description.")]
    public string model_description { get; set; }

    [Required]
    [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
    public DateTime created_date { get; set; }

    [Required]
    public string created_by { get; set; }

    [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
    public DateTime modified_date { get; set; }

    public string modified_by { get; set; }
}

EDIT:

Added View code:

Input Form:

        <span class="control-label col-md-2">Type:</span>
        <div class="col-md-4">
            @Html.DropDownListFor(model => model.Type_Id, (SelectList)ViewBag.Model_List, "<<< CREATE NEW >>>", htmlAttributes: new { @class = "form-control dropdown" })
            @Html.ValidationMessageFor(model => model.Type_Id, "", new { @class = "text-danger" })
        </div>
        <div class="col-md-1">
            <div class="btn-group">
                <button type="button" class="btn btn-success" aria-expanded="false">CREATE NEW</button>
            </div>
        </div>

SCRIPT:

        $('#submitNewModel').click(function () {

            var form = $(this).closest('form');
            var data = { model_description: document.getElementById('textNewModel').value };

            $.ajax({
                type: "POST",
                dataType: "JSON",
                url: '@Url.Action("createNewModel", "INV_Assets")',
                data: data,
                success: function (resp) {
                    alert("SUCCESS!");
                    $('#selectModel').append($('<option></option>').val(resp.ID).text(resp.Text));
                    alert("ID: " + resp.ID + " // New Model: " + resp.Text); // RETURNING 'undefined'...?
                    form[0].reset();
                    $('#createModelFormContainer').hide();
                },
                error: function () {
                    alert("ERROR!");
                }
            });
        });
tereško
  • 58,060
  • 25
  • 98
  • 150
Analytic Lunatic
  • 3,853
  • 22
  • 78
  • 120

3 Answers3

8

When you cannot quickly deduce why your ModelState validation fails, it's often helpful to quickly iterate over the errors.

foreach (ModelState state in ModelState.Values.Where(x => x.Errors.Count > 0)) { }

Alternatively you can pull out errors directly.

var allErrors = ModelState.Values.SelectMany(x => x.Errors);

Keep in mind that the ModelState is constructed BEFORE the body of your Action is executed. As a result, IsValid will already be set, regardless of how you set your model's properties once you are inside of the Controller Action.

If you want the flexibility to manually set properties and then re-evalute the validity of the object, you can manually rerun the validation inside of your Action after setting the properties. As noted in the comments, you should clear your ModelState before attempting to revalidate.

ModelState.Clear();
ValidateModel(model);

try
{
    if (ModelState.IsValid)
    {
        db.INV_Models.Add(model);
        db.SaveChangesAsync();
    }
}
...

As an aside, if the model is still not valid ValidateModel(model) will throw an exception. If you'd like to prevent that, use TryValidateModel, which returns true/false instead:

protected internal bool TryValidateModel(Object model)
David L
  • 32,885
  • 8
  • 62
  • 93
  • Thanks for replying David. I tried out `ValidateModel()` and `TryValidateModel()`, but both are still returning as `Invalid`. I then used your approach with `allErrors` and put a watch on `allErrors`. Drilling down into the `[Results View]=>[0]=>[ErrorMessage]` I get `"The created_by field is required."` I have confirmed though that I AM specifying the `created_by` value...? – Analytic Lunatic Feb 13 '15 at 19:26
  • @AnalyticLunatic model.created_by = System.Environment.UserName; Are you certain that this is not null? Remember, [Required] will return invalid if your value is null on a reference type. – David L Feb 13 '15 at 20:54
  • Correct, `model.created_by = System.Environment.UserName` is not `null`. I got it to work by doing a `ModelState.Clear()` BEFORE I attempt `TryValidateModel(model)`. Once that occurs, everything is functioning as intended. I also had to change to `db.SaveChanges()` vs `db.SaveChangesAsync()` or else the value would only get added to DB if I had a Breakpoint. Without, the code would process to quickly and return before the DB add occurred. – Analytic Lunatic Feb 13 '15 at 21:15
  • Interesting, I don't see anything in your code that would require db.SaveChangesAsync();. Regardless, I'll update my answer to reflect the need for ModelState.Clear(); – David L Feb 13 '15 at 21:24
3

You should not be using a hack like ModelState.Clear() nor is TryValidateModel(model); required. Your issue stems from the fact you have a [Required] attribute on both your created_date and created_by properties but you don't post back a value, so they are null and validation fails. If you were to post back a more complex model, then you would use a view model which did not even have properties for created_date and created_by (its a Create method, so they should not be set until you post back).

In your case a view model is not necessary since your only posting back a single value ( for model-description) used to create a new INV_Models model.

Change the 2nd line in the script to

var data = { description: $('#textNewModel').val() };

Change your post method to

[HttpPost]
public JsonResult createNewModel(string description)
{
  // Initialize a new model and set its properties
  INV_Models model = new INV_Models()
  {
    model_description = description,
    created_date = DateTime.Now,
    created_by = System.Environment.UserName
  };
  // the model is valid (all 3 required properties have been set)
  try
  {
    db.INV_Models.Add(model);
    db.SaveChangesAsync();
  }
  catch (Exception ex)
  {
    Elmah.ErrorSignal.FromCurrentContext().Raise(ex);
  }
  return Json( new { ID = model.Id, Text = model.model_description }, JsonRequestBehavior.AllowGet);
}

Side notes:

  1. I suggest modified_date be DateTime? (nullable in database also). You are creating a new object, and are setting the created_date and created_by properties, but setting modified_date and modified_by properties does not seem appropriate (it hasn't been modified yet).
  2. I suspect you don't really want to set created_by to System.Environment.UserName (it would be meaningless to have every record set to administrator or whatever UserName of the server returns. Instead you need to get users name from Identity or Membership whatever authorization system you are using.
  • That's why I was so lost without using the `ModelState.Clear()` & `TryValidate()`. All 3 properties with `[Required]` were being given a value. The `Description` passed to the controller via JSON, and then `created_date`/`created_by` being given a value within the controller (right before saving), but without clearing the state and revalidating, I was still getting an invalid model error. Regarding side notes, I have implemented #1 -- Did `DateTime?` on the main class, but not the other ones for some reason. #2 - Not sure what method will be used yet. Just a current idea pending review. – Analytic Lunatic Feb 17 '15 at 21:45
  • I thought the `ModelState.Clear()` etc. could be avoid if I passed a value for `created_by` and `created_date` along with my `model_description` for my `data` variable, but when I attempt `var data = { model_description: document.getElementById('textNewModel').value, created_date: @DateTime.Now, created_by: @System.Environment.UserName };` the script never executes to the controller, failing with: "Unexpected Number" - `var data = { model_description: document.getElementById('textNewModel').value, created_date: 2/17/2015 3:51:15 PM, created_by: JSmith };`...? – Analytic Lunatic Feb 17 '15 at 21:54
  • 1
    The reason you were having that problem is because you method parameter was `INV_Models model`. The default `ModelBinder` first initializes a new instance of the model, then sets its properties based on the posted values. Because you don't post anything for `created_by` its value is the default (`null`) but then a model state error is added because of the `Required` attribute. Just make the parameter `string description` as above. –  Feb 17 '15 at 21:54
  • 1
    You should not be trying to send a `created_date` from the view. A user could have started doing it yesterday, get distracted and finish it off today. Your code would set the created date in the database as yesterdays date (i.e the wrong date!) because `@DateTime.Now` is the date when the page was first loaded. –  Feb 17 '15 at 22:01
  • Right, right. I think I see what you are saying now. I didn't realize you had changed the Controller parameter in the above code. I will be attempting your suggestion and see if all works.I believe I understand now what you mean about the `ModelBinder` looking at one instance of my model whereas my parameter is initializing another. – Analytic Lunatic Feb 17 '15 at 22:04
  • Thanks Stephen, I've implemented your example above and everything appears to be functioning as expected -- though I do wonder something. When/Where exactly does (I'm assuming `ModelBinding` or during `Save()`) the `ID` value get set behind the scenes for a new Model record? – Analytic Lunatic Feb 18 '15 at 15:26
  • If your `ID` property is an auto-incremented key in the data base, EF will update the value when you add a new record when you call `db.SaveChangesAsync();` - but why are you doing this as async? –  Feb 19 '15 at 00:22
  • Honestly for no other reason than that is how the method was generated due to me checking `[X] create async methods` when creating the `Controller`. – Analytic Lunatic Feb 19 '15 at 20:27
1

The model state is calculated when the binding from your post data to model is done. The ModelState.IsValid property only tells you if there are some errors in ModelState.Errors.

When you set your created date you will need to remove the error related to it from ModelState.Errors

Mike Norgate
  • 2,393
  • 3
  • 24
  • 45
  • Thanks for the reply Mike. I didn't realize the `ModelState` was checked during the binding call (though that makes sense). I've confirmed using David's approach that the `ModelState` seems to think it's invalid due to the `created_by` field being required -- but I am specifying that value?? – Analytic Lunatic Feb 13 '15 at 19:30
  • @AnalyticLunatic As I mentioned in my answer you will need to remove the error from the `ModelState`. See the answer to this question http://stackoverflow.com/questions/2588588/setting-modelstate-values-in-custom-model-binder – Mike Norgate Feb 13 '15 at 20:10
  • Sorry, I thought that `TryValidateModel()` and `ValidateModel()` would clear the error and do a new Validation? What exactly would I specify to `Remove`? – Analytic Lunatic Feb 13 '15 at 20:12
  • 1
    @AnalyticLunatic you would need to specify the same key as in `ModelState.Errors` which I believe would be `"created_date"` – Mike Norgate Feb 13 '15 at 20:17
  • Thanks Mike! I'm not sure which would be better, but I just got it to work by calling `ModelState.Clear()` before `TryValidateModel(model)`. – Analytic Lunatic Feb 13 '15 at 20:19
  • Correction, it worked once... and now nothing. It appears that if I walk through the code where it adds the new description to DB, it works. But if I let it run without a break point, the new description is added to dropdown but NOT the DB. I'm assuming maybe `db.SaveChangesAsync()` is not fully completing before the return occurs? – Analytic Lunatic Feb 13 '15 at 20:27