填充ViewModel的正确方式是什么?

4

我正在为工作开发一个Web应用程序,并使用标准的CRUD风格交互。然而,有些字段我不希望用户更新,所以我从视图中将它们移除。但是,如果我没有明确设置这些字段,在模型在数据库中更新时,它们就会被清除。

我关心的是正确的填充ViewModels字段的方法。

我想到的大致思路是这样的:

我的视图模型:

public class EditSoftwareTrackingViewModel 
{
    public EditSoftwareTrackingViewModel(SoftwareTracking model)
    {
        Id = model.Id;
        SoftwareId = model.SoftwareId;
        ComputerId = model.ComputerId;
        SoftwareActionId = model.SoftwareActionId;
        LastModified = model.LastModified;
        Computer = model.Computer;
        Software = model.Software;
        SoftwareAction = model.SoftwareAction;
    }
    public int Id { get; set; }
    [DisplayName("Software")]
    public int SoftwareId { get; set; }
    [DisplayName("Computer")]
    public int ComputerId { get; set; }
    [DisplayName("Software Action")]
    public int SoftwareActionId { get; set; }
    [DisplayName("Last Modified")]
    public DateTime? LastModified { get; set; }

    public virtual Computer Computer { get; set; }
    public virtual Software Software { get; set; }
    public virtual SoftwareAction SoftwareAction { get; set; }
}

我的主要模型

[Table("asset.SoftwareTracking")]
public partial class SoftwareTracking
{
    public int Id { get; set; }
    [DisplayName("Software")]
    public int SoftwareId { get; set; }
    [DisplayName("Computer")]
    public int ComputerId { get; set; }
    [DisplayName("Date Entered")]
    public DateTime? EnteredDate { get; set; }
    [DisplayName("Software Action")]
    public int SoftwareActionId { get; set; }
    [DisplayName("Last Modified")]
    public DateTime? LastModified { get; set; }

    public virtual Computer Computer { get; set; }
    public virtual Software Software { get; set; }
    public virtual SoftwareAction SoftwareAction { get; set; }
}

我的控制器使用视图模型

    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        EditSoftwareTrackingViewModel softwaretracking = new EditSoftwareTrackingViewModel(db.SoftwareTrackings.Find(id));
        if (softwaretracking == null)
        {
            return HttpNotFound();
        }
        GeneratePageData(softwaretracking.Software.Id);
        return View(softwaretracking);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(EditSoftwareTrackingViewModel softwaretracking)
    {
        if (ModelState.IsValid)
        {
            softwaretracking.LastModified = DateTime.Now;
            var softwareTrack = db.SoftwareTrackings.Find(softwaretracking.Id);
            softwareTrack = new SoftwareTracking
            {
                Computer = softwaretracking.Computer,
                ComputerId = softwaretracking.ComputerId,
                LastModified = softwaretracking.LastModified,
                Software = softwaretracking.Software,
                SoftwareAction = softwaretracking.SoftwareAction,
                SoftwareActionId = softwaretracking.SoftwareActionId,
                SoftwareId = softwaretracking.SoftwareId,
                EnteredDate = softwareTrack.EnteredDate
            };

            db.Entry(softwareTrack).State = EntityState.Modified;
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        GeneratePageData(softwaretracking.Software.Id);
        return View(softwaretracking);
    }

有更好的替代方案吗?还是我应该继续以这种方式创建我的视图模型?
编辑
我的业务逻辑和视图。
    private void GeneratePageData(int? id = null)
    {

        ViewBag.Computers = new SelectList(db.Computers, "Id", "ComputerName");
        ViewBag.SoftwareActions = new SelectList(db.SoftwareActions, "Id", "ActionPerformed");

        var usedSoft = (from softTrack in db.SoftwareTrackings
                        where (softTrack.SoftwareActionId != 3)
                        select softTrack.Software);

        var softwareList = (from soft in db.Softwares
                            where (
                                ((from softTrack in db.SoftwareTrackings
                                  where (softTrack.SoftwareActionId != 3 && softTrack.SoftwareId == soft.Id)
                                  select softTrack.Software).Count() < soft.KeyQuantity)
                                && !(soft.AssetStatusId == 4 || soft.AssetStatusId == 5)
                                || soft.Id == id)
                            select soft).ToList();

        ViewBag.SoftwareList = softwareList.Select(t => new SelectListItem
        {
            Text = t.SoftwareIdNameFull,
            Value = t.Id.ToString()
        });

    }

我的观点是

@model Lighthouse_Asset_Manager.Models.EditSoftwareTrackingViewModel

@{
    ViewBag.Title = "Edit Software Install";
    Layout = "";
}

<div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">
        &times;
    </button>
    <h4 class="modal-title" id="myModalLabel">Edit Software Install</h4>
</div>
<div class="modal-body">

    @using (Html.BeginForm(null, null, FormMethod.Post, new { id = "computerForm" }))
    {
        @Html.AntiForgeryToken()
        @Html.HiddenFor(model => model.Id)

        <div class="form-horizontal">
            @Html.ValidationSummary(true)


            <div class="form-group">
                @Html.LabelFor(model => model.SoftwareId, "Software", new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.DropDownList("SoftwareId", (IEnumerable<SelectListItem>)ViewBag.SoftwareList, "-- Select --", new
                    {
                        @style = "width:100%",
                        @class = "select2"
                    })
                    @Html.ValidationMessageFor(model => model.SoftwareId)
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.ComputerId, "Computer", new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.DropDownList("ComputerId", (IEnumerable<SelectListItem>)ViewBag.Computers, "-- Select --", new
                    {
                        @style = "width:100%",
                        @class = "select2"
                    })
                    @Html.ValidationMessageFor(model => model.ComputerId)
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.SoftwareActionId, "Action", new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.DropDownList("SoftwareActionId", (IEnumerable<SelectListItem>)ViewBag.SoftwareActions, "-- Select --", new
                    {
                        @style = "width:100%",
                        @class = "form-control"
                    })
                    @Html.ValidationMessageFor(model => model.SoftwareActionId)
                </div>
            </div>


            <div class="form-actions no-color">
                <button type="submit" class="btn btn-primary btn-sm"><i class="fa fa-floppy-o"></i> Edit Install Record</button>
                <button type="button" class="btn btn-default" data-dismiss="modal">
                    Cancel
                </button>
            </div>
        </div>
    }
</div>

1
使用视图模型是正确的方法,但在您的POST方法中,您需要从数据库中获取原始的SoftwareTracking对象,并将您的视图模型属性映射到它,然后保存原始对象。顺便说一下:您不需要(也不应该)在POST方法中调用GeneratePageData(softwaretracking.Software.Id);(基于原始模型重置视图模型属性是被忽略的)。 - user3559349
我已经更新了我的代码,使其实际运行,但是,我不确定还有什么其他方法可以使其更清晰、更高效。我使用GeneratePageData的原因是因为我在ViewBag中填充了某些列表,这些列表在视图中显示。这些列表是否从get方法中传递? - JD Davis
2
不,你需要在返回视图之前重新填充任何选择列表 - 如果这是它所做的一切,那么没问题。由于你有一个视图模型,应该将 SelectList 属性包含在视图模型中,而不是在 ViewBag 中,你的视图模型包含一个似乎不必要的 LastModified 属性(当你回传并保存数据模型时,在控制器中设置值即可 - 我假设这不是用户可以编辑的内容)。你还可以查看像 automapper 这样的工具,使其更加简单和清晰。 - user3559349
1
我同意。我只是建议您的视图模型包含SelectLists属性,您仍然有一个通用的私有方法来分配这些属性,在GET和(假设您返回视图)POST方法中调用该方法。我使用类似于private void ConfigureEditModel(MyViewModel model) { model.MyFirstSelectList = someLinqQuery; }的东西。 - user3559349
@Jdsfighter 他的意思是,不要设置 ViewBag.SomeList = complex_query,而是使用 softwaretrackingView.SomeList = complex_query - wal
显示剩余6条评论
4个回答

4
您采用的视图模型方法是不错的。 这个问题的答案解释了一些好处,包括防止过度发布攻击,使用特定于视图的显示和验证属性以及包含特定于视图的属性,如SelectLists。像automapper这样的工具可以轻松地在您的数据和视图模型之间进行映射,并减少控制器中的代码。我建议对您的视图模型进行一些更改。 LastModifiedComputerSoftwareSoftwareAction属性不是必需的(您没有绑定到这些属性),并且我会在模型中包含SelectList属性,而不是使用ViewBag。
视图模型
public class EditSoftwareTrackingViewModel 
{
  public int Id { get; set; }
  [Display(Name="Software")]
  public int SoftwareId { get; set; }
  [Display(Name="Computer")]
  public int ComputerId { get; set; }
  [Display(Name="Software Action")]
  public int SoftwareActionId { get; set; }
  public SelectList Computers { get; set; }
  public SelectList SoftwareActions{ get; set; }
  public SelectList SoftwareList{ get; set; }
}

接下来将GeneratePageData()方法更改为接受视图模型

private void GeneratePageData(EditSoftwareTrackingViewModel model)
{
  model.Computers = new SelectList(db.Computers, "Id", "ComputerName");
  ....

在视图中(始终优先使用强类型帮助器)

@Html.DropDownListFor(m => m.SoftwareId, Model.SoftwareList, "-- Select --", new { @class = "select2" })

一些需要注意的事项:
  • 应该使用[Display(Name="..")]特性(而不是[DisplayName(..)]
  • 当设置LastModified属性时,应考虑使用UCT时间。
  • Id属性的隐藏输入在视图中不是必需的(假设您使用默认的{controller}/{action}/{id}路由映射)-它被添加到路由值并将被绑定。
  • 除非您特别需要表单的id属性,否则可以只使用@using(Html.BeginForm()) {
  • LabelFor()中不需要第二个参数-它可以只是Html.LabelFor(m => m.SoftwareId, new { @class = "control-label col-md-2" }),因为您已在[Display]特性中指定了它
最后,如果您想进一步简化视图,您可以考虑自定义EditorTemplates或html helpers,如这个答案所示,这将允许您替换。
<div class="form-group">
  @Html.LabelFor(model => model.SoftwareId, new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DropDownListFor(m => m.SoftwareId, Model.SoftwareList, "-- Select --", new { @class = "select2" })
    @Html.ValidationMessageFor(model => model.SoftwareId)
  </div>
</div>

使用自定义EditorTemplate

@Html.EditorFor(m => m.SoftwareId, "BootstrapSelect", Model.SoftwareList)

或者(自定义HtmlHelper
@Html.BootstrapDropDownFor(m => m.SoftwareId, Model.SoftwareList)

那里有一些非常棒的建议。非常感谢您的贡献。 - JD Davis
顺便提一下,您提到从模型设置LastModified属性。如何实现这一点呢?我似乎无法理解。 - JD Davis
假设您不希望用户编辑它,则仅在控制器中保存之前设置其值(DateTime.Now)的时间是在控制器中(即将视图模型映射到数据模型,然后设置数据模型的最后修改日期,然后保存数据模型)。 - user3559349
是的,我知道,只是之前你在视图模型中有属性,然后更新了视图模型,然后将其映射到数据模型(前两个步骤并不是必要的 :) - user3559349
我开始使用AutoMapper,它大大简化了流程。然而,我开始意识到Visual Studio内置的脚手架工具经常以一种有些奇怪的方式处理事情,我需要对生成的代码进行很多更正。 - JD Davis
显示剩余2条评论

2
您应该使用AutoMapper工具来使模型(Model)和视图模型(ViewModel)之间的映射更加清晰。请使用以下代码先创建映射器:
Mapper.CreateMap<SoftwareTracking, EditSoftwareTrackingViewModel>();
Mapper.CreateMap<EditSoftwareTrackingViewModel, SoftwareTracking>();

当您想从模型创建视图模型时,请执行以下操作:

public ActionResult Edit(int? id)
{
    SoftwareTracking tracking = db.SoftwareTrackings.Find(id);
    EditSoftwareTrackingViewModel viewmodel = 
        Mapper.Map<SoftwareTracking, EditSoftwareTrackingViewModel>(tracking);
    return View(viewmodel);
}

当您想要从ViewModel填充信息回到Model时,请执行以下操作:

public ActionResult Edit(EditSoftwareTrackingViewModel vm)
{
    if (ModelState.IsValid)
    {
        vm.LastModified = DateTime.Now;
        var softwareTrack = db.SoftwareTrackings.Find(softwaretracking.Id);
        softwareTrack = 
           Mapper.Map<EditSoftwareTrackingViewModel, SoftwareTracking>(vm, softwareTrack);

        db.Entry(softwareTrack).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }

1

如果要更新模型而不从数据库中加载对象,请尝试使用 Attach

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(EditSoftwareTrackingViewModel softwaretracking)
    {
        if (ModelState.IsValid)
        {
            var softwareTrack = new SoftwareTracking
            {
                 Computer = softwaretracking.Computer,
                 ComputerId = softwaretracking.ComputerId,
                 LastModified = softwaretracking.LastModified,
                 Software = softwaretracking.Software,
                 SoftwareAction = softwaretracking.SoftwareAction,
                 SoftwareActionId = softwaretracking.SoftwareActionId,
                 SoftwareId = softwaretracking.SoftwareId,
                 EnteredDate = softwareTrack.EnteredDate
            };
            db.SoftwareTrackings.Attach(softwareTrack);

            db.Entry(softwareTrack).Property(a => a.Computer).IsModified = true;
            db.Entry(softwareTrack).Property(a => a.ComputerId).IsModified = true;
            db.Entry(softwareTrack).Property(a => a.LastModified).IsModified = true;
            db.Entry(softwareTrack).Property(a => a.Computer).IsModified = true;
            db.Entry(softwareTrack).Property(a => a.Software).IsModified = true;
            db.Entry(softwareTrack).Property(a => a.SoftwareAction).IsModified = true;

            db.Entry(softwareTrack).Property(a => a.SoftwareActionId).IsModified = true;
            db.Entry(softwareTrack).Property(a => a.SoftwareId).IsModified = true;
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        GeneratePageData(softwaretracking.Software.Id);
        return View(softwaretracking);
    }

关于第二个问题,关于使用ViewModel还是直接使用Model。这其实是一个观点问题,每种方法都有其优缺点。我对此没有强烈的意见,只是想指出以下优缺点供您考虑:
  • 直接使用Model可以让我们避免创建ViewModel,从而减少源代码量并避免映射逻辑,但会导致混淆职责。因为您将同一模型用于领域逻辑和与客户端通信,如果不考虑到这一点,对模型进行的任何更改都可能传播到客户端。
  • 使用ViewModel是一种良好的分离职责的方式,但需要更多的努力和映射逻辑(可能会稍微降低性能)。为了有效地应用ViewModel,建议使用Mapper:https://github.com/AutoMapper/AutoMapper/wiki/Getting-started

如果我选择这条路,使用ViewModel将是多余的,因为使用ViewModel消除字段是不必要的。我可以简单地标记我想要更改的字段。 - JD Davis
@Jdsfighter:您必须告诉实体框架需要更新哪些字段,因为实体框架无法意识到您在另一个上下文中更新数据。在您的情况下,您可能需要将 ViewModel 转换为 Model,并标记要更新的字段。或者,如果两个模型之间没有太多不匹配之处,您可以直接使用 Model。 - Khanh TO
这些模型是相同的,只是从 ViewModel 中排除了某些属性。 - JD Davis
@Jdsfighter:如果存在不匹配的情况,我们通常会创建一个 Mapper 来从 ViewModel 和 Model 进行映射,以便我们可以重用此映射代码。一个很好的库是 https://github.com/AutoMapper/AutoMapper/wiki/Getting-started - Khanh TO
@Jdsfighter:通常,ViewModel 应该从 Model 中“展平”,不包含复杂的领域结构。 - Khanh TO
@Jdsfighter:在某些情况下,使用viewModel与否是一个观点问题。我会两种方式都可以。每种方式都有其优缺点。 - Khanh TO

1
这是模型类。
[Table("CURRENCY")]
    public class CurrencyClass : ICurrency
    {
        private Int32 mCURRENCY_ID = default(Int32);
        [Key]
        public virtual Int32 CURRENCY_ID  
        {
            get { return mCURRENCY_ID; }
            set { mCURRENCY_ID = value; }
        }
        private string mCURRENCY_NAME = default(string); 
        public virtual string CURRENCY_NAME 
        { 
            get { return mCURRENCY_NAME;}
            set { mCURRENCY_NAME = value;}
        }
        private string mCURRENCY_DESC = default(string);
        public  virtual string CURRENCY_DESC 
        {
            get { return mCURRENCY_DESC; }
            set { mCURRENCY_DESC = value; }
        }
        private string mCURRENCY_SYMBOLE = default(string);
        public virtual string CURRENCY_SYMBOLE 
        {
            get { return mCURRENCY_SYMBOLE; }
            set { mCURRENCY_SYMBOLE = value; }
        }
        private Int32 mcreated_by = default(Int32);
        public virtual Int32 created_by 
        {
            get { return mcreated_by; }
            set { mcreated_by = value; } 
        }
        private DateTime mcreated_date = default(DateTime);
        public virtual DateTime created_date 
        {
            get { return mcreated_date; }
            set { mcreated_date = value; } 
        }
        private Int32 mmodified_by = default(Int32);
        public virtual Int32 modified_by 
        {
            get { return mmodified_by; }
            set { mmodified_by = value; } 
        }
        private DateTime mmodified_date = default(DateTime);
        public virtual DateTime modified_date 
        {
            get { return mmodified_date; }
            set { mmodified_date = value; }
        }
    }

这是ViewModel。
public class CurrencyViewModel
    {
        [Key]
        public Int32 CURRENCY_Id { get; set; }
        [Required(ErrorMessage="Currency Name is required")]
        public string CURRENCY_NAME { get; set; }
        [Required(ErrorMessage="Currency Description is required")]
        public string CURRENCY_DESC { get; set; }
        [Required(ErrorMessage = "Currency Symbole is Required")]
        public string CURRENCY_SYMBOLE { get; set; }
    }

这是一个动作。
[HttpPost]
        [ActionName("Create")]
        public ActionResult Create(CurrencyViewModel vm)
        {
            if (!ModelState.IsValid)
            {
                return View("Create");
            }   

            obj.CURRENCY_NAME = vm.CURRENCY_NAME;
            obj.CURRENCY_DESC = vm.CURRENCY_DESC;
            obj.CURRENCY_SYMBOLE = vm.CURRENCY_SYMBOLE;
            obj.created_by = 1;
            obj.created_date = DateTime.Now;
            obj.modified_by = 1;
            obj.modified_date = DateTime.Now;
            db.Currencies.Add(obj);
            db.SaveChanges();

            return RedirectToAction("Index");
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接