【问题标题】:ASP.NET MVC 4 Navigation Virtual Property not populated on Post Action后操作未填充 ASP.NET MVC 4 导航虚拟属性
【发布时间】:2013-03-12 17:12:01
【问题描述】:

我在 Question 类上有一个导航属性 (Category),我在 Question 的 Create 视图中手动为其创建 DropDownList ,并且在发布 Create 操作时,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 正确保存到数据库中,这不会发生。

谢谢

【问题讨论】:

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


    【解决方案1】:

    代替

            @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

    【讨论】:

    • 我知道没有自动填充导航属性的方法,所以我明确澄清了这一点,但我发现我必须发出单独的查询来填充属性,这样我才能指定,这很荒谬问题属于哪个类别。应该有一种手动设置 categoryid(整数)的方法,并且不需要填充整个属性。有吗?
    • 好吧,我将需要更改问题标题,但您的回答确实很有效。我不关心表单发布时自动填充的属性(它甚至没有意义),我只需要将我发送的 CategoryId 值存储在相应的问题记录中,现在就是这样,但只是因为我在 Question 类中添加了 CategoryId 属性,所以仅使用 Category 导航属性是不可能的。
    【解决方案2】:

    我知道这个问题已经得到解答,但它让我思考。

    所以我想我找到了一种通过一些约定做到这一点的方法。

    首先,我让实体继承自这样的基类:

    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 where T : Entity { 私有只读 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 容器。

    就是这样。你有办法自动进行绑定。

    【讨论】:

    • 嘿VinTem。与往常一样,实验是好的,因为我们学到了更多,起初,我实际上和你一样,按照惯例做,但务实地思考,自定义活页夹确实增加了开销和复杂性,而我所需要的只是要正确保存的 CategoryId。谢谢!
    • 我明白了。我只是想在遇到其他类似情况时找到解决问题的方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-23
    • 2013-11-18
    相关资源
    最近更新 更多