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.
И это все. У вас есть способ сделать эту привязку автоматически.