【问题标题】:View Model virtual properties and drop down lists查看模型虚拟属性和下拉列表
【发布时间】:2014-02-05 18:42:12
【问题描述】:

我无法掌握正确的方法来创建视图模型并使用 Entity Framework 将该信息保存回数据库,而且我似乎无法找到我正在寻找的信息,所以请原谅我忽略了。

我看到了这个帖子here,他似乎也在问同样的问题,但没有得到答案。

我的主要问题是,

出于编辑目的,如果我有一个ProductModel modelWarranty model 关系,我应该在视图模型中使用virtual property Warranty 还是应该使用int WarrantyId

如果我应该使用虚拟属性,为什么这段代码不能正确保存Warranty

我是否需要明确标记或填写保修以进行更新?

请不要这样填充我的编辑视图并按预期选择列表。

我的(简化的)代码设置如下:

型号:

    public int ModelId{ get; set; }

    public int ModelNumber { get; set; }

    public virtual Warranty Warranty { get; set;}

查看模型:

    public int ModelId { get; set; }

    [Required(ErrorMessage = "Model Number required")]
    [StringLength(25, ErrorMessage = "Must be under 25 characters")]
    [Display(Name="Model Number")]
    public string ModelNumber { get; set; }

    //related objects and necesary properties
    public virtual Warranty Warranty { get; set; }

    public IEnumerable<SelectListItem> WarrantySelectListItems { get; set; }

控制器(GET):

public ActionResult Edit(int? id)
    {
        //check the id
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }

        //get the model and make sure the object is populated
        var model = _modelService.GetModel(id.Value);
        if (model == null)
        {
            return HttpNotFound();
        }

        //pass our entity (db) model to our view model
        var editModelModel = new EditModelModel();
        editModelModel.InjectFrom(model);

        //warranty select list
        editModelModel.WarrantySelectListItems = WarrantySelectList(editModelModel.Warranty.WarrantyId);

        //option multi select list
        editModelModel.OptionSelectListItems = OptionSelectList();

        return View(editModelModel);
    }

控制器(POST)(正在进行中):

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(EditModelModel editModelModel)
    {
        if (!ModelState.IsValid)
        {
            return View(editModelModel);
        }

        var modelEntity = new Model();
        modelEntity.InjectFrom(editModelModel);

        _modelService.Update(modelEntity);
        _unitOfWork.Save();

        return RedirectToAction("Index");
    }

查看(简化):

<div class="form-group">
        @Html.Label("Warranty", new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.DropDownListFor(x => x.Warranty.WarrantyId, Model.WarrantySelectListItems, "--Select--")
            @Html.ValidationMessageFor(model => model.Warranty.WarrantyId)
        </div>
    </div>

再次,我只想知道设置这些视图模型和模型的正确/最佳方法,以便 EF 尽可能多地完成工作。 我觉得如果我必须创建一个 WarrantyId 字段,我做错了什么,但也许不是这样。

提前致谢。非常感谢任何见解/帮助。

【问题讨论】:

    标签: c# asp.net-mvc entity-framework


    【解决方案1】:

    出于编辑目的,如果我的 ProductModel 模型具有 保修模型关系,我应该使用虚拟财产吗 视图模型中的保修还是我应该使用 int WarrantyId?

    您不要使用virtual 关键字作为ViewModel 的属性,因为ViewModel 与实体框架无关。 使用virtual 关键字的原因是允许实体框架中的延迟加载。在您的情况下,如果您添加 virtual 关键字 Product POCO 类中的 Warranty 导航属性,您可以访问 Warranty 属性,如下所示:

    Model.Warranty.WarrantyId
    

    它没有将保修信息保存到您的数据库中的原因是您需要在 Product 类中定义一个保修外键属性。

    在您的情况下,如果您使用的是代码优先方法,并且 Product 是您的 POCO 类,请保持如下所示:

        public class Product
        {
            public int ModelId { get; set; }
            public int ModelNumber { get; set; }
            public int WarrantyId {get;set;}
    
            [ForeignKey("WarrantyId ")]
            public virtual Warranty Warranty { get; set; }
        }
    

    然后是你的 ViewModel :

        public class MyViewModel 
        {
            public Product Product { get; set; }
            public IEnumerable<SelectListItem> WarrantySelectListItems { get; set; }
        }
    

    最后是你的观点

      @model MyViewModel
    
      @Html.DropDownList("Product.Warranty.WarrantyId", Model.WarrantySelectListItems, "--Select--")
      @Html.ValidationMessageFor("Product.Warranty.WarrantyId")
    

    当然,你需要改变你的动作方法来满足 ViewModel。

    【讨论】:

    • 虽然其他几个答案非常好,但我接受这个作为这个问题的答案,因为它回答了直接提出的问题,并给出了如何在给定范围内解决我的模型视图模型问题的示例原始问题的界限。此外,它的支持率最高。
    【解决方案2】:

    出于编辑目的,如果我有一个具有 Warranty 模型关系的 ProductModel 模型,我应该在视图模型中使用虚拟属性 Warranty 还是应该使用 int WarrantyId?

    您不应该在视图模型中使用虚拟属性。视图模型仅表示显示视图所需的数据片段。当您从实体映射到该视图模型时,您不需要将任何东西标记为虚拟。如果你想知道virtual 对实体框架做了什么,请参阅this answer

    此外,您应该只包含呈现该视图所需的信息。因此,如果您只需要视图中的 WarrantyId,则只需包含它。

    由于您还在 POST 操作中将模型绑定回同一视图模型,因此您应该非常具体地说明您希望您的视图模型表示什么,否则您将向over-posting attack 敞开大门。

    我觉得如果我必须创建一个 WarrantyId 字段,我做错了什么,但也许不是这样。

    事实并非如此。你的每一个观点都应该是独立的。当您第一次开始使用视图模型时,每个视图一个,您最初的反应是违反DRY。但是,每个视图都有不同的要求。就视图模型本身而言,最明显的区别是验证。如果您在视图中使用实体,则所有这些视图都与您应用于实体的验证规则相关联。 (如果您不希望用户能够编辑整个实体,您也很容易过度发布。)

    但是,通过为您的视图设置单独的视图模型,并在视图模型本身上应用验证规则,您现在可以在视图中拥有不同的验证要求。例如:

    public class ViewAViewModel
    {
        [Required]
        public int WarrantyId { get; set; }
    }
    
    public class ViewBViewModel
    {
        // No longer required.
        public int WarrantyId { get; set; }
    }
    

    如果您将Warranty 直接包含在这两个视图中,您将被一组验证规则所困。

    除此之外,我想知道为什么你的模型上有这个(我假设它是一个实体):

    public IEnumerable<SelectListItem> WarrantySelectListItems { get; set; }
    

    那不属于这里。这是一个演示细节,它不应该存在于您的业务对象中。它应该存在于您的视图模型中。

    【讨论】:

    • 实际上,您想知道的那部分是我的错字。选择列表位于视图模型上。我已经编辑以反映这一点。
    • @MikeMcCoy 那很好,因为那是它所属的地方。
    • 谢谢你的回答,很有用
    【解决方案3】:

    您正在处理的绝对是导航属性(模型类上的虚拟属性),这很好地解释了它们:

    http://msdn.microsoft.com/en-us/data/jj713564.aspx

    定义这些的棘手部分实际上在于如何为数据库设置 DbContext。官方文档在这里:

    http://msdn.microsoft.com/en-us/data/jj591620

    简单的父子关系很容易处理,还有其他情况(更棘手),您可以在其中定义物理上来自表中同一行的多个模型,但我认为您不是在这里处理。

    MVC 部分是一个单独的关注点,理想情况下您应该这样对待它。控制器代码应该只将真正的“工作”委托给其他类。如果您选择使用工作单元模式,那么在您遇到需要在许多表或实体集上保存/编辑大量内容的情况之前,它并不是真正必要的,并且您认为您可以希望这一切都失败或整体成功。如果您只是处理单个对象的简单持久性,甚至不要将其与工作单元模式复杂化。

    EF 或任何 ORM 框架要记住的另一件事是,它需要跟踪更改或与现有记录进行比较,因此在您完成此操作时,关键值变得非常重要。

    【讨论】:

    • 感谢杰夫提供的信息。一定会帮助我的!
    【解决方案4】:

    ViewModel 是数据的简化视图,可感知 UI,仅包含 UI 呈现和用户输入所需的信息。

    做更多的工作似乎是错误的——为什么不直接使用模型呢?但是对于复杂的系统,您最终会变得非常复杂,并且您通常需要更改模型以适应 UI,这很混乱。

    此外,ViewModel 允许您在没有数据库且不复杂的情况下测试 UI。您确实将 UI 问题和数据建模问题解耦了。

    我通常最终从不在 UI 上使用模型,总是通过 ViewModel 来简化我的生活,即使它首先需要更多的工作。

    所以让我们做一些改变。

    视图模型(为清晰起见重命名为 EditViewModel):

    public int ModelId { get; set; }
    
    // Removed for clarity, include needed properties in the UI
    
    public int WarrantyId { get; set; }
    
    public IEnumerable<SelectListItem> WarrantySelectListItems { get; set; }
    

    控制器(GET):

    public ActionResult Edit(int? id)
    {
        //check the id
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
    
        //get the model and make sure the object is populated
        var model = _modelService.GetModel(id.Value);
        if (model == null)
        {
            return HttpNotFound();
        }
    
        //pass our entity (db) model to our view model
        var editViewModel = new EditViewModel();
        editViewModel.InjectFrom(model);
    
        // You could instead create a custom injection like FlatLoopValueInjection
        // That would flatten and remove duplicates from 
        // Model.Warranty.WarrantyId to ViewModel.WarrantyId
        editViewModel.WarrantyId = model.Warranty.Id;
    
        //warranty select list
        editViewModel.WarrantySelectListItems = WarrantySelectList(editViewModel.WarrantyId);
    
        return View(editViewModel);
    }
    

    自定义注入扁平化 - FlatLoopValueInjection:

    http://valueinjecter.codeplex.com/wikipage?title=flattening&referringTitle=Home

    控制器(POST):

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(EditViewModel editViewModel)
    {
        if (!ModelState.IsValid)
        {
            return View(editViewModel);
        }
    
        // You need to reconstruct the model itself, there are faster ways but I wanted
        // to showcase the logic behind it
        // I didn't do any null check or anything to simplify
    
        // Load the model used from the database
        var modelEntity = _modelService.GetModel(editViewModel.ModelId);
    
        // You can do an InjectFrom for the other properties you need
        // with custom Injection to unflatten
        modelEntity.InjectFrom(editViewModel);
    
        // Load the selected warranty from the database
        var warrantyEntity = _warrantyService.GetWarranty(editViewModel.WarrantyId);
    
        // Update the warranty of the model with the one loaded
        modelEntity.Warranty = warrantyEntity;
    
        _modelService.Update(modelEntity);
    
        _unitOfWork.Save();
    
        return RedirectToAction("Index");
    }
    

    现在在你看来:

    <div class="form-group">
        @Html.Label("Warranty", new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.DropDownListFor(x => x.WarrantyId, Model.WarrantySelectListItems, "--Select--")
            @Html.ValidationMessageFor(model => model.WarrantyId)
        </div>
    </div>
    

    附带说明,在您的模型和视图模型中,您应该尽量不要在名称中重复前缀,例如:

    • Model.ModelId
    • Warranty.WarrantyId

    除非是外键或值:

    • Model.WarrantyId

    为什么?使用 InjectFrom 按照惯例将它们展平/取消展平要容易得多:

    Model.Warranty.Id => (flatten) => Model.WarrantyId => (unflatten) => Model.Warranty.Id
    

    此外,这是一种最佳做法。模型/表的名称已经告诉你实体类型,无需重复。

    【讨论】:

      【解决方案5】:

      您的视图模型中必须有 int WarrantyId。 比在你看来

      @Html.DropDownListFor(x => x.WarrantyId, Model.WarrantySelectListItems, "--Select--")
      

      在控制器 (POST) 中获取 WarrantyId(从下拉列表中选择)并从数据库中查找对象(var 保证 = db.Warranties.Where(w=>w.WarrantyId == editModelModel.WarrantyId 或类似的东西)并将该对象分配到模型实体。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多