ASP.NET MVC 4 Виртуальное свойство навигации не заполняется при выполнении действия

У меня есть свойство навигации (Категория) в классе Вопрос, для которого я вручную создаю DropDownList для представления "Создание вопроса", и при публикации действия "Создать" свойство навигации "Категория" не заполняется в Модели, поэтому я получаю недействительный ModelState.

Вот моя модель:

 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; }
    }

Вот мой контроллер вопросов для действий GET и POST команды 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);
        }

А вот и представление Создать вопрос

@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>
}

Я пробовал следующие варианты создания выпадающего списка в представлении:

@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")

Когда я быстро наблюдаю за объектом "Вопрос" в действии POST, свойство "Категория" имеет значение null, но в поле "Категория" этого свойства устанавливается значение "Выбранная категория" в представлении.

Я знаю, что мог бы легко добавить код для ручной выборки категории с помощью EF, используя значение CategoryId, полученное из представления. Я также думаю, что мог бы создать для этого пользовательское связующее, но я надеялся, что это можно сделать с помощью аннотаций данных.

Я что-то пропустил?

Есть ли лучший способ создать выпадающий список для свойства навигации?

Есть ли способ дать MVC знать, как заполнить свойство навигации без необходимости вручную это делать?

-- РЕДАКТИРОВАТЬ:

Если это имеет какое-то значение, мне не нужно загружать фактическое свойство навигации при создании / сохранении Вопроса, мне просто нужно правильно сохранить CategoryId в базе данных, чего не происходит.

Спасибо

2 ответа

Решение

Вместо

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

Пытаться

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

Редактировать:

Не существует автоматического способа заполнить свойство Navigation по идентификатору, опубликованному в форме. Потому что для получения данных должен быть выполнен запрос к базе данных, и он не должен быть прозрачным. Это должно быть сделано явно. Более того, выполнение этой операции в пользовательском связывателе, вероятно, не лучший способ. В этой ссылке есть хорошее объяснение: внедрить зависимость в пользовательский связыватель модели и использовать InRequestScope с помощью Ninject.

Я знаю, что на этот вопрос уже дан ответ, но он заставил меня задуматься.

Поэтому я думаю, что нашел способ сделать это с некоторыми соглашениями.

Во-первых, я сделал сущности наследующими от базового класса следующим образом:

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

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

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

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

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

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

Итак, я также изменил модель Question, чтобы не иметь дополнительного свойства с именем CategoryId.

Для формы все, что я сделал, было:

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

Итак, вот второе соглашение, вам нужно, чтобы поле свойства было названо с суффиксом Id.

Наконец, CustomModelBinder и CustomModelBinderProvider

public class CustomModelBinderProvider : IModelBinderProvider
    {
        private readonly IKernel _kernel;

        public CustomModelBinderProvider(IKernel kernel)
        {
            _kernel = kernel;
        }

        public IModelBinder GetBinder(Type modelType)
        {
            if (!typeof(Entity).IsAssignableFrom(modelType))
                return null;

            Type modelBinderType = typeof (CustomModelBinder<>)
                .MakeGenericType(modelType);

            // I registered the CustomModelBinder using Windsor
            return _kernel.Resolve(modelBinderType) as IModelBinder;
        }
    }

открытый класс CustomModelBinder: DefaultModelBinder где T: Entity {private readonly QuestionsContext _db;

public CustomModelBinder(QuestionsContext db)
{    
    _db = db;
}

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    var model = base.BindModel(controllerContext, bindingContext) as T;

    foreach (var property in typeof(T).GetProperties())
    {
        if (property.PropertyType.BaseType == typeof(Entity))
        {
            var result = bindingContext.ValueProvider.GetValue(string.Format("{0}Id", property.Name));
            if(result != null)
            {
                var rawIdValue = result.AttemptedValue;
                int id;
                if (int.TryParse(rawIdValue, out id))
                {
                    if (id != 0)
                    {
                        var value = _db.Set(property.PropertyType).Find(id);
                        property.SetValue(model, value, null);
                    }
                }
            }
        }
    }
    return model;
}

}

CustomModelBinder будет искать свойства типа Entity и загружать данные с переданным Id, используя EF.

Здесь я использую Windsor для внедрения зависимостей, но вы можете использовать любой другой контейнер IoC.

И это все. У вас есть способ сделать эту привязку автоматически.

Другие вопросы по тегам