Search code examples
asp.net-mvcasp.net-mvc-4data-annotationsmodelbindersnavigation-properties

ASP.NET MVC 4 Navigation Virtual Property not populated on Post Action


I have a navigation property (Category) on a Question class for which I am manually creating a DropDownList for in the Create view of Question, and when posting the Create action, the Category navigation property is not populated on the Model, therefore giving me an invalid ModelState.

Here is my model:

 public class Category
    {
        [Key]
        [Required]
        public int CategoryId { get; set; }

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

        public virtual List<Question> Questions { get; set; }
    }

public class Question
    {
        [Required]
        public int QuestionId { get; set; }

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

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

        [ForeignKey("CategoryId")]
        public virtual Category Category { get; set; }

        public int CategoryId { get; set; }
    }

Here is my Question controller for both GET and POST actions of Create:

public ActionResult Create(int? id)
        {
            ViewBag.Categories = Categories.Select(option => new SelectListItem {
                Text = option.CategoryName,
                Value = option.CategoryId.ToString(),
                Selected = (id == option.CategoryId)
            });
            return View();
        }


        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Question question)
        {
            if (ModelState.IsValid)
            {
                db.Questions.Add(question);
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            return View(question);
        }

And here is the Create view for Question

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Question</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Category)
        </div>
        <div class="editor-field">
            @Html.DropDownListFor(model => model.Category.CategoryId, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.QuestionText)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.QuestionText)
            @Html.ValidationMessageFor(model => model.QuestionText)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Answer)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Answer)
            @Html.ValidationMessageFor(model => model.Answer)
        </div>

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

I have tried the following variations of generating the dropdownlist on the view:

@Html.DropDownListFor(model => model.Category.CategoryId, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
@Html.DropDownListFor(model => model.Category, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
@Html.DropDownList("Category", (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
@Html.DropDownList("CategoryId", (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")

When I quickwatch the Question object on the POST action, the Category property is null, but the CategoryId field on the property is set to the selected Category on the view.

I know I could easily add code to manually fetch the Category with EF by using the CategoryId value that I get from the view. I also think I could create a custom binder to do this, but I was hoping that this could be done with data annotations.

Am I missing something?

Is there a better way to generate the dropdownlist for the navigation property?

Is there a way to let MVC know how to populate the navigation property without me having to manually do it?

-- EDIT:

If it makes any difference, I do not need the actual navigation property loaded when creating/saving the Question, I just need the CategoryId to be correctly saved to the Database, which isn't happening.

Thanks


Solution

  • Instead of

            @Html.DropDownListFor(model => model.Category.CategoryId, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
    

    Try

            @Html.DropDownListFor(model => model.CategoryId, (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
    

    Edit:

    There is no automatic way to populate Navigation property from the id posted from the form. Because, a database query should be issued to get the data and it should not be transparent. It should be done explicitly. Moreover, doing this operation in a custom binder probably probably is not the best way. There is a good explanation in this link : Inject a dependency into a custom model binder and using InRequestScope using Ninject