ASP.NET MVC - 从视图部分更新模型

16

我想知道人们如何处理这种情况。这似乎是我在使用MVC和ORM(在本例中为NHibernate)时的一个薄弱点...

假设您的模型中有一个细粒度且复杂的实体。您可能会有一个管理此类型对象的管理员页面。如果实体很复杂,那么您不太可能在一个表单中修改整个实体。您仍然需要将相关属性传递到视图,并在视图返回它们时将这些属性的更改合并到模型中。

在这种情况下,任何人该怎么办呢?

  • 创建一个视图模型,其中包含实体属性的子集或全部属性。将其传递给视图,并从视图获取它。在控制器中的“编辑”操作方法中,从存储库获取对象,遍历ViewModel中的所有属性,并将它们应用于Model对象(model.a = viewmodel.a,model.b = viewmodel.b)。这似乎是明显而合理的路径,但会产生大量繁琐的管道代码。同时,这也会稍微增加验证的复杂性。

  • 还有别的吗?

我简单地查看了自动映射器——但似乎并不完全符合要求,也许我错了?

谢谢。


我使用一个视图模型,你完全正确,它确实会导致一些枯燥的左手右手代码。根据我的了解,AutoMapper可以减轻部分这种代码。 - Aaron
是的,我有这种感觉,这就是必须的方式。不过你永远不知道,也许有人会有一些巧妙的技巧... - UpTheCreek
6个回答

11
这听起来像是automapper的完美应用场景。您创建一个包含真实模型字段子集的视图模型类,然后让AutoMapper从域模型对象中提取值到您的视图模型对象中。您在这种方法中遇到了哪些问题?
考虑以下示例:
这是您的领域模型和视图模型。
public class Person
{
    public string FirstName
    { get; set; }

    public string LastName
    { get; set; }

    public string HomeNumber
    { get; set; }

    public string Address1
    { get; set; }

    public string Address2
    { get; set; }
}

public class PersonViewModel
{
    public string FirstName
    { get; set; }

    public string LastName
    { get; set; }

    public string HomeNumber
    { get; set; }
}

这是您的映射,您需要创建一个从dm->vm和vm->dm的双向映射。
根据我使用Automapper的经验,如果您将A对象映射到B对象,并且B具有A没有的属性,则该属性将被重置。因此,在创建映射时,我会指示忽略那些缺失的属性。我不是Automapper专家,所以可能使用不正确。
映射
Mapper.CreateMap<Person, PersonViewModel>();
// Automapper will reset values in dest which don't exist in source, so be sure to ignore them!
Mapper.CreateMap<PersonViewModel, Person>()
    .ForMember(dest => dest.HomeNumber, opt => opt.Ignore());

最后使用:

Person p = new Person()
{
    FirstName = "First",
    LastName = "Last",
    Address1 = "add 1",
    Address2 = "add 2"
};

PersonViewModel pvm = Mapper.Map<Person, PersonViewModel>(p);
// Map to a new person
Person p2 = Mapper.Map<PersonViewModel, Person>(pvm);

// Map to the existing person just to update it
Person p3 = new Person()
{
    HomeNumber = "numberHere"
};

// This will update p3
Mapper.Map<PersonViewModel, Person>(pvm, p3);

由于这种排除方式,显然不是最理想的解决方案,但比全手动完成要好得多。

谢谢Sayed,这对于“仅查看”情况很好。但据我所知,当视图中的值被编辑后(例如在“编辑”管理员表单情况下),automapper无法将其映射回域模型对象。也许我错了? - UpTheCreek
我为你添加了一个例子。 - Sayed Ibrahim Hashimi
感谢提供详细的示例,看起来 AutoMapper 的功能比我想象的要更多。我会再做些研究。 - UpTheCreek

5
  1. 让您的视图模型与您的领域模型一一对应。

  2. routeValues参数中指定Model。这意味着您的视图模型将使用来自领域模型的值进行初始化。表单中的子集字段将被覆盖为结果中的personViewData

更新视图:

@model ViewModel.PersonView

@using (Html.BeginForm("Update", "Profile", Model, FormMethod.Post))
{
  ...Put your sub set of the PersonView fields here
}

ProfileController:

public ActionResult Update(string userName)
{
    Person person = _unitOfWork.Person.Get().Where(p => p.UserName == userName).FirstOrDefault();
    PersonView personView = new PersonView();
    Mapper.Map(person, personView);

    return View(personView);
}

[HttpPost]
public ActionResult Update(PersonView personViewData)
{
   Person person = _unitOfWork.Person.Get().Where(p => p.UserName == personViewData.UserName).FirstOrDefault();
   Mapper.Map(personViewData, person);
   _unitOfWork.Person.Update(person);
   _unitOfWork.Save();

   return Json(new { saved = true, status = "" });
}

2

为什么不使用TryUpdateModel与表单集合一起使用。

如果您的视图正在编辑一个人

public class Person
{
    public string ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Address { get; set; }
}

您的视图只编辑名字和姓氏,您可以这样做:

public ActionResult Action(FormCollection form)
{
    Person personToUpdate = Repository.GetPerson(form["ID"]);

    TryUpdateModel<Person>(personToUpdate, form);

    Repository.Update(personToUpdate)

    return View();
}

这将只使用表单集合中的项目更新"Person"。如果您不想更新某个字段,请不要将其与表单一起提交。


我不确定地说,希望能得到一些反馈。我认为你不想这样做是因为:1. 它的类型不够强,2. 它不能与自定义模型绑定器结合使用。Levi 在某个地方解释过,updatemodel 只用于更新视图模型。 - Martin

1

我使用了与你类似的方法(在我的情况下是Entity Framework),即Entity->ViewModel->View,但仅适用于具有1:M或M:M关系的“复杂”实体的视图。在大多数情况下,当我有一个简单的实体时,我会采用Entity->View的低级路线。

我的ViewModel被定义为Entity+支持属性:SelectListMultiSelectList以及stringList<string>。我还将在需要视图中的属性但可能不一定需要在实体(数据库)中的情况下使用ViewModel。

Http Get控制器方法是简单的ActionResults,其中Create使用return View(repository.FetchNewViewModel()),而Edit使用repository.FetchModelById(id)。在这两种情况下,我都会在传递给视图之前初始化我的实体。

Create([Bind(Exclude = "Entity.EntityId")] ViewModel model)Edit(ViewModel model)是Create和Edit的Http Post控制器方法。我的Edit视图有一个隐藏的输入字段用于传递EntityId。

当 Http Post 方法得到 ViewModel 时,我会失去所有的 Entity.Relation 和 ViewModel.(Multi)SelectList 值。如果我想要正确显示视图,我必须重新构建对象:

try
{
    var tags = model.TagIds; // List<string> or <int> depending on your Id type
    if (model.TagsList == null) // It will be
    {
        model.TagsList = _repository.FetchSelectedTags(tags); // Build a new SelectList
    }

    if (!ModelState.IsValid)
    {
        return View(model);
    }

    _repository.Add(model.Article, tags); // or Save() for Edit
}
catch
{
    return View(model); // Generally means something screwed in the repository class
}

return RedirectToAction("Index");

`

我的实体库可能只有30%使用ViewModel,所以我只在需要时使用它。如果您有复杂的视图和模型数据,在大多数情况下,您可能可以将其拆分为较小的视图。


1

如果您拥有完整的模型,但每个页面仅使用和更新所需的部分,那么您可以在最后一页使用完整视图数据来更新业务模型。


谢谢。我认为你在建议一种多页“向导”样式的表单?我并不是在讨论需要分小块更新整个模型的情况,而是仅涉及传递模型以进行更新的详细信息。 - UpTheCreek
不,我认为他并没有这样做。我认为他建议不要使用视图模型。只需传递完整的模型,并使操作仅更改/更新所需的属性即可。通过删除VM对象的额外管道来简化该过程。 - Matt Kocaj
那么,“使用最后一页的完整视图数据更新业务模型”这一位呢? - UpTheCreek

0

我现在正在使用S#arp Architecture开发一个大型项目,同时也采用了以下方法:

Model -> ViewModel -> Model

我在绑定部分和验证方面使用ViewModel,另一种方法是直接使用Model(使用tryUpdateModel / UpdateModel)这种方式我们在原型开发期间使用过,但对于复杂场景,我们最终还是要处理像SelectLists / Checkbox / Projections / HMAC Validations这样的特殊情况,在ViewModel中仍然需要使用大量的Request.Form [“key”] =(),另一个缺点是处理错误情况时,您希望重新填充用户输入表单,我发现直接使用Model会更加复杂(使用ViewModel,我们可以充分利用ModelState attempted value,节省了几次访问数据库的时间,任何遇到这种情况的人都知道我的意思)。

这种方法有点耗时,就像你说的那样,你最终会匹配属性,但在我看来,对于复杂的表单而言,这是可行的方法。

值得一提的是,我们仅在创建/编辑方案中使用ViewModels,对于几乎所有其他情况,我们直接使用model。

我迄今为止还没有使用过自动映射器,但肯定会尝试一下。


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