在Post Action上未填充ASP.NET MVC 4导航虚拟属性

时间:2013-03-12 17:12:01

标签: asp.net-mvc asp.net-mvc-4 data-annotations modelbinders navigation-properties

我在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正确保存到数据库,这是没有发生的。

由于

2 个答案:

答案 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容器。

就是这样。你有办法自动生成绑定。