ASP.NET виртуальное свойство навигации MVC 4 не заполняется при выполнении Post Action
У меня есть свойство навигации (Category) в классе вопросов, для которого я вручную создаю раскрывающийся список в представлении Create вопроса, и при отправке действия Create свойство навигации по категориям не заполняется в модели, поэтому дает мне недопустимое состояние 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);
}
А вот представление Create для вопроса
@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")
Когда я быстро просматриваю объект Question в действии POST, свойство Category имеет значение null, но поле CategoryId в этом свойстве имеет значение выбранной категории в представлении.
Я знаю, что могу легко добавить код для ручной выборки категории С помощью EF, используя значение CategoryId, которое я получаю из представления. Я также думаю, что я мог бы создать пользовательский связующий файл, чтобы сделать это, но я надеялся, что это может быть сделано с аннотациями данных.
Я что-то упустил?
Есть ли лучший способ создать раскрывающийся список для свойства навигации?
Есть ли способ дать MVC знать, как заполнить свойство навигации без необходимости делать это вручную?
-- EDIT:
Если это имеет какое-либо значение, мне не нужно фактическое свойство навигации, загруженное при создании/сохранении вопроса, мне просто нужно, чтобы 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")Правка:
Не существует автоматического способа заполнения свойства навигации по идентификатору, опубликованному в форме. Потому что для получения данных должен быть выдан запрос к базе данных, и он не должен быть прозрачным. Это должно быть сделано явно. Более того, выполнение этой операции в пользовательской папке, вероятно, не самый лучший способ. Существует хорошее объяснение в этой ссылке: введите зависимость в пользовательскую модель binder и с помощью 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; } }Итак, я также изменил модель вопроса, чтобы не иметь дополнительного свойства, называемого 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: сущность { 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 и загружать данные с переданным идентификатором, используя EF.
Здесь я использую Windsor для введения зависимостей,но вы можете использовать любой другой контейнер IoC.
И это все. У тебя есть способ сделать эту привязку автоматической.
Comments