如何减轻运行时创建视图模型的痛苦


17

对于这个很长的问题,我深表歉意。我在下面总结了我的问题

在MVC世界中,事情很简单。该模型具有状态,在视图显示模式,并且控制器东西/与模型(基本上),控制器没有的状态。为了完成这些工作,Controller对Web服务,存储库等有一些依赖性。实例化控制器时,您关心的是提供那些依赖关系,而没有别的。执行动作(Controller上的方法)时,可以使用这些依赖关系来检索或更新Model或调用某些其他域服务。如果存在任何上下文,例如说某些用户想要查看特定项目的详细信息,则将该项目的ID作为参数传递给Action。Controller中的任何地方都没有对任何状态的引用。到目前为止,一切都很好。

输入MVVM。我爱WPF,我喜欢数据绑定。我喜欢使数据绑定到ViewModels更加容易的框架(使用Caliburn Micro atm)。我觉得这个世界上事情并不那么简单。让我们再次做运动:该模型具有状态下,查看显示的视图模型和视图模型的东西/使用模型(基本),一个视图模型有状态!(澄清;也许它代表所有属性的一个或多个模型,但是这意味着它必须具有对模型的一种方式或另一种,这本身就是状态的基准)为了ViewModel在Web服务,存储库等方面有一些依赖性。实例化ViewModel时,您关心的是提供那些依赖关系,还需要提供状态。女士们,先生们,这无休止地困扰着我。

每当您需要从中实例化a ProductDetailsViewModelProductSearchViewModel(您又从中实例化了ProductSearchWebServicewhich IEnumerable<ProductDTO>,每个人还和我在一起吗?),您可以执行以下操作之一:

  • call new ProductDetailsViewModel(productDTO, _shoppingCartWebService /* dependcy */);,这很糟糕,想象另外3个依赖关系,这意味着也ProductSearchViewModel需要承担这些依赖关系。改变构造函数也是很痛苦的。
  • 调用_myInjectedProductDetailsViewModelFactory.Create().Initialize(productDTO);,工厂只是一个Func,大多数IoC框架很容易生成它们。我认为这很糟糕,因为Init方法是一个泄漏的抽象。您也不能对Init方法中设置的字段使用readonly关键字。我确定还有更多原因。
  • 呼叫_myInjectedProductDetailsViewModelAbstractFactory.Create(productDTO);So ...这是通常建议用于此类问题的模式(抽象工厂)。我虽然是个天才,但它满足了我对静态类型的渴望,直到我真正开始使用它为止。我认为样板代码太多了(除了我使用的荒谬变量名之外,您都知道)。对于每个需要运行时参数的ViewModel,您将获得两个额外的文件(工厂接口和实现),并且需要键入非运行时依赖项,例如额外输入4次。而且,每次依赖关系发生变化时,您也需要在工厂中进行更改。感觉我什至不再使用DI容器。(我认为温莎城堡对此有某种解决方案(有其自身的缺点,如果我错了,请纠正我))。
  • 用匿名类型或字典来做某事。我喜欢我的静态打字。

是的。以这种方式混合状态和行为会产生一个在MVC中根本不存在的问题。我觉得目前还没有一个真正足够的解决方案来解决这个问题。现在,我想观察一些事情:

  • 人们实际上使用了MVVM。因此,他们要么不在乎以上所有内容,要么拥有一些出色的其他解决方案。
  • 我还没有找到带有WPF的MVVM的深入示例。例如,NDDD示例项目极大地帮助我理解了一些DDD概念。如果有人可以将我指向类似MVVM / WPF的方向,我真的很喜欢它。
  • 也许我在做MVVM时出错了,应该将我的设计倒过来。也许我根本不应该有这个问题。好吧,我知道其他人也问过同样的问题,所以我认为我不是唯一的一个。

总结一下

  • 我是否可以正确地得出结论,将ViewModel作为状态和行为的集成点是整个MVVM模式存在某些困难的原因?
  • 使用抽象工厂模式是以静态类型实例化ViewModel的唯一/最佳方法吗?
  • 是否有类似深度参考实现的内容?
  • 是否有很多带有状态/行为的ViewModels设计气味?

10
阅读本书的时间太长了,可以考虑进行修订,其中有很多无关紧要的内容。您可能会错过好答案,因为人们不会理会所有内容。
yannis

您说过您喜欢Caliburn.Micro,但是您不知道该框架如何帮助实例化新的视图模型?检查它的一些示例。
欣快的2012年

@Euphoric您能否更具体一点,Google似乎在这里没有帮助我。有一些我可以搜索的关键字?
dvdvorle 2012年

3
我认为您正在简化MVC。确保视图在开始时显示了模型,但是在操作过程中它正在改变状态。在我看来,这种变化的状态是“编辑模型”。也就是说,模型的扁平版本具有减少的一致性限制。实际上,我所谓的“编辑模型”是MVVM ViewModel。它在过渡时保持状态,该状态先前由MVC中的View保持,或者被推回到模型的未提交版本中,我认为它不属于该状态。因此,您之前具有“不断变化”的状态。现在全部在ViewModel中。
Scott Whitlock,2012年

@ScottWhitlock我确实是在简化MVC。但是我并不是说ViewModel中处于“不断变化”状态是错误的,我是说在其中充斥着行为也使得将ViewModel从另一个ViewModel初始化为可用状态变得更加困难。您在MVC中的“编辑模型”不知道如何保存自身(它没有Save方法)。但是控制器确实知道这一点,并且具有执行此操作所需的所有依赖项。
dvdvorle 2012年

Answers:


2

可以使用IOC处理启动新视图模型时的依赖关系问题。

public class MyCustomViewModel{
  private readonly IShoppingCartWebService _cartService;

  private readonly ITimeService _timeService;

  public ProductDTO ProductDTO { get; set; }

  public ProductDetailsViewModel(IShoppingCartWebService cartService, ITimeService timeService){
    _cartService = cartService;
    _timeService = timeService;
  }
}

设置容器时...

Container.Register<IShoppingCartWebService,ShoppingCartWebSerivce>().As.Singleton();
Container.Register<ITimeService,TimeService>().As.Singleton();
Container.Register<ProductDetailsViewModel>();

当您需要视图模型时:

var viewmodel = Container.Resolve<ProductDetailsViewModel>();
viewmodel.ProductDTO = myProductDTO;

当使用诸如caliburn micro之类的框架时,通常已经存在某种形式的IOC容器。

SomeCompositionView view = new SomeCompositionView();
ISomeCompositionViewModel viewModel = IoC.Get<ISomeCompositionViewModel>();
ViewModelBinder.Bind(viewModel, view, null);

1

我每天都使用ASP.NET MVC,并且在WPF上工作了一年多,这是我的看法:

MVC

控制器应该安排动作(获取,添加)。

该视图负责显示模型。

该模型通常包含数据(例如UserId,FirstName)和状态(例如标题),并且通常是特定于视图的。

MVVM

该模型通常仅保存数据(例如UserId,FirstName),并且通常随处传递

视图模型包含视图的行为(方法),其数据(模型)和交互(命令),类似于演示者意识到模型的活动MVP模式。视图模型是特定于视图的(1个视图= 1个视图模型)。

视图负责显示数据以及将数据绑定到视图模型。创建视图时,通常会与其一起创建其关联的视图模型。


您应该记住的是,MVVM呈现模式由于其数据绑定性质而特定于WPF / Silverlight。

视图通常知道与哪个视图模型相关联(或一个视图的抽象)。

我建议您将视图模型视为单例,即使它是针对每个视图实例化的。换句话说,您应该能够通过IOC容器通过DI创建它,并在其上调用适当的方法来说;根据参数加载其模型。像这样:

public partial class EditUserView
{
    public EditUserView(IContainer container, int userId) : this() {
        var viewModel = container.Resolve<EditUserViewModel>();
        viewModel.LoadModel(userId);
        DataContext = viewModel;
    }
}

作为这种情况下的示例,您将不会创建特定于要更新的​​用户的视图模型-而是该模型将包含通过对视图模型的某些调用加载的特定于用户的数据。


如果我的名字是“ Peter”,而我的标题是{“ Rev”,“ Dr”} *,为什么要考虑名字数据和标题状态?还是可以澄清您的例子?*不是真的
Pete Kirkham 2013年

@PeteKirkham-我说的是“组合框”上下文中的“标题”示例。通常,当您发送要保留的信息时,您不会发送用于进行选择的州(例如州/省/标题的列表)。在处理时,应检查任何与数据传输的有价值状态(例如,正在使用的用户名),因为状态可能已过时(如果您正在使用某些异步模式,例如消息队列)。
Shelakel

尽管距这篇文章已经两年了,但我必须对未来的观众发表评论:有两件事困扰着您。一个View可能对应一个ViewModel,但是一个ViewModel可以由多个View表示。其次,您要描述的是服务定位器反模式。恕我直言,您不应该在任何地方直接解析视图模型。那就是DI的目的。尽您所能解决问题。例如,让Caliburn为您完成这项工作。
乔尼·阿当密特2015年

1

您问题的简短答案:

  1. 是的,状态+行为会导致这些问题,但这对于所有OO都是如此。真正的罪魁祸首是ViewModels的耦合,这是一种违反SRP的行为。
  2. 可能是静态类型。但是您应该减少/消除从其他ViewModel实例化ViewModel的需求。
  3. 并不是我知道。
  4. 否,但是具有状态和行为无关的ViewModel(例如一些Model引用和一些ViewModel引用)

长版:

我们正面临着同样的问题,并且发现了一些可能对您有所帮助的事情。尽管我不知道“魔术”解决方案,但是这些事情在减轻一些痛苦。

  1. 从DTO实施可绑定模型,以进行更改跟踪和验证。那些“ Data” -ViewModels一定不能依赖服务,也不能来自容器。它们可以只是“新的”,可以传递,甚至可以来自DTO。底线是实现特定于您的应用程序的模型(如MVC)。

  2. 解耦您的ViewModels。Caliburn使将ViewModel耦合在一起变得容易。它甚至通过其屏幕/导体模型来建议。但是,这种耦合使ViewModels难以进行单元测试,创建了大量依赖项,并且最重要:在ViewModels上增加了管理ViewModel生命周期的负担。解耦它们的一种方法是使用导航服务或ViewModel控制器之类的东西。例如

    公共接口IShowViewModels {void Show(object inlineArgumentsAsAnonymousType,string regionId); }

更好的是通过某种形式的消息传递来做到这一点。但是重要的是不要处理其他ViewModel的ViewModel生命周期。在MVC中,控制器不相互依赖,在MVVM中,ViewModel不应该相互依赖。通过其他方式将它们集成。

  1. 使用容器的“字符串”类型/动态功能。尽管可以创建类似的内容INeedData<T1,T2,...>并强制执行类型安全的创建参数,但这是不值得的。为每种ViewModel类型创建工厂也不值得。大多数IoC容器为此提供了解决方案。在运行时会出错,但是去耦和单元可测试性值得。您仍然需要进行某种集成测试,并且很容易发现这些错误。

0

我通常这样做的方式(使用PRISM)是每个程序集都包含一个容器初始化模块,其中所有接口,实例都在启动时注册。

private void RegisterResources()
{
    Container.RegisterType<IDataService, DataService>();
    Container.RegisterType<IProductSearchViewModel, ProductSearchViewModel>();
    Container.RegisterType<IProductDetailsViewModel, ProductDetailsViewModel>();
}

在给定示例类的情况下,将像这样实现,容器将一直通过。这样,由于您已经可以访问容器,因此可以轻松添加任何新的依赖项。

/// <summary>
/// IDataService Interface
/// </summary>
public interface IDataService
{
    DataTable GetSomeData();
}

public class DataService : IDataService
{
    public DataTable GetSomeData()
    {
        MessageBox.Show("This is a call to the GetSomeData() method.");

        var someData = new DataTable("SomeData");
        return someData;
    }
}

public interface IProductSearchViewModel
{
}

public class ProductSearchViewModel : IProductSearchViewModel
{
    private readonly IUnityContainer _container;

    /// <summary>
    /// This will get resolved if it's been added to the container.
    /// Or alternately you could use constructor resolution. 
    /// </summary>
    [Dependency]
    public IDataService DataService { get; set; }

    public ProductSearchViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void SearchAndDisplay()
    {
        DataTable results = DataService.GetSomeData();

        var detailsViewModel = _container.Resolve<IProductDetailsViewModel>();
        detailsViewModel.DisplaySomeDataInView(results);

        // Create the view, usually resolve using region manager etc.
        var detailsView = new DetailsView() { DataContext = detailsViewModel };
    }
}

public interface IProductDetailsViewModel
{
    void DisplaySomeDataInView(DataTable dataTable);
}

public class ProductDetailsViewModel : IProductDetailsViewModel
{
    private readonly IUnityContainer _container;

    public ProductDetailsViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void DisplaySomeDataInView(DataTable dataTable)
    {
    }
}

有一个ViewModelBase类是很常见的,您所有的视图模型都派生自该类,其中包含对容器的引用。只要您养成解决所有视图模型而不是解决new()'ing它们的习惯,它就应该使所有依赖关系解析变得更加简单。


0

有时候,最好使用最简单的定义,而不是使用完整的示例:http : //en.wikipedia.org/wiki/Model_View_ViewModel 也许阅读ZK Java示例比C#更具启发性。

其他时候听你的直觉...

是否有很多带有状态/行为的ViewModels设计气味?

您的模型是按表映射的对象吗?ORM可能会在处理业务或更新多个表时帮助映射到域对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.