我在Question类上有一个导航属性(Category),我在Question的Create视图中为其手动创建DropDownList,当发布Create动作时,Model上没有填充Category导航属性,因此给我一个无效的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操作:
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操作上快速观察Question对象时,Category属性为null,但属性上的CategoryId字段设置为视图上的选定类别。
我知道我可以通过使用从视图中获取的CategoryId值轻松添加代码以手动获取带有EF的类别。我也认为我可以创建一个自定义绑定器来执行此操作,但我希望可以使用数据注释完成此操作。
我错过了什么吗?
有没有更好的方法来为导航属性生成下拉列表?
有没有办法让MVC知道如何在不必手动操作的情况下填充导航属性?
- 编辑:
如果它有任何区别,我不需要在创建/保存问题时加载实际的导航属性,我只需要将CategoryId正确保存到数据库,这是没有发生的。
由于
答案 0 :(得分:4)
而不是
@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")
编辑:
没有自动方法从表单中发布的id填充Navigation属性。因为,应该发出数据库查询来获取数据,它不应该是透明的。它应该明确地完成。而且,在自定义绑定器中执行此操作可能不是最好的方法。这个链接有一个很好的解释:Inject a dependency into a custom model binder and using InRequestScope using Ninject
答案 1 :(得分:1)
我知道这个问题已经回答了,但它让我思考。
所以我认为我找到了一种方法,用一些约定。
首先,我让实体继承自这样的基类:
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;
}
}
public class 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类型的属性,并使用EF为传递的Id加载数据。
这里我使用Windsor注入依赖项,但您可以使用任何其他IoC容器。
就是这样。你有办法自动生成绑定。