使用MVVM在WPF中创建新窗口的最佳方法

57
在邻居帖子中:如何使用ViewModel关闭窗体? 我发表了我的看法,关于如何使用MVVM关闭窗口。现在我有一个问题:如何打开它们。
我有一个主窗口(主视图)。如果用户单击“显示”按钮,则应显示“演示”窗口(模态对话框)。使用MVVM模式创建和打开窗口的首选方法是什么?我看到两种一般性的方法:
第一种方法(可能是最简单的)。"ShowButton_Click"事件处理程序应该在主窗口的代码后台中实现,像这样:
        private void ModifyButton_Click(object sender, RoutedEventArgs e)
        {
            ShowWindow wnd = new ShowWindow(anyKindOfData);
            bool? res = wnd.ShowDialog();
            if (res != null && res.Value)
            {
                //  ... store changes if neecssary
            }
        }
  1. 如果我们需要更改“显示”按钮的状态(启用/禁用),我们需要添加逻辑来管理按钮状态。
  2. 源代码与“旧式”WinForms和MFC源代码非常相似 - 我不确定这是好还是坏,请建议。
  3. 还有其他遗漏的东西吗?

另一种方法:

在MainWindowViewModel中,我们将实现“ShowCommand”属性,该属性将返回命令的ICommand接口。而命令本身:

  • 将引发“ShowDialogEvent”;
  • 将管理按钮状态。

这种方法更适合MVVM,但需要额外的编码:ViewModel类无法“显示对话框”,因此MainWindowViewModel仅会引发“ShowDialogEvent”。MainWindowView需要在其MainWindow_Loaded方法中添加事件处理程序,例如:

((MainWindowViewModel)DataContext).ShowDialogEvent += ShowDialog;

(ShowDialog - 类似于 'ModifyButton_Click' 方法。)

所以我的问题是: 1. 你有没有看到其他的方法? 2. 你认为列出的其中一种好还是坏?(为什么?)

欢迎分享任何其他想法。

谢谢。


1
我在这篇帖子中回答了一个类似的问题,涉及到一个非常简单的行为。 - Mike Fuchs
ViewModel类不能“显示对话框” <---为什么?因为这将违反MVVM模式? - monstr
6个回答

18
一些MVVM框架(例如MVVM Light)使用中介者模式。因此,为了打开一个新窗口(或创建任何视图),一些特定于视图的代码将订阅中介者的消息,并且ViewModel将发送这些消息。

像这样:

订阅

Messenger.Default.Register<DialogMessage>(this, ProcessDialogMessage);
...
private void ProcessDialogMessage(DialogMessage message)
{
     // Instantiate new view depending on the message details
}

在ViewModel中

Messenger.Default.Send(new DialogMessage(...));

我更倾向于在单例类中进行订阅,这个类的“生命”与应用程序的UI部分一样长。

总之:ViewModel传递诸如“我需要创建一个视图”的消息,而UI侦听这些消息并对其进行操作。

当然,并没有“理想”的方法。


1
嗨,arconaut,您介意澄清一下,您实际上将订阅代码放在哪里?是视图的后台代码中吗? - nabeelfarid
2
它可以位于某些“静态”视图的代码后端,例如主窗口。或者最好放在一个负责实例化视图的单独类中。 - arconaut

16
最近我也在思考这个问题。如果您在项目中使用Unity作为依赖注入的“容器”或其他,我有一个想法。我猜通常你会重写App.OnStartup()并创建你的模型、视图模型和视图,然后给每个适当的引用。使用Unity,您将给容器一个对模型的引用,然后使用容器来“解析”视图。Unity容器注入您的视图模型,因此您永远不会直接实例化它。一旦您的视图被解析,您就调用Show()方法显示它。
在我观看的一个示例视频中,Unity容器是在OnStartup中作为局部变量创建的。如果您在App类中将其创建为一个公共的静态只读属性呢?您可以在主视图模型中使用它来创建新窗口,自动注入新视图所需的任何资源,比如像这样:App.Container.Resolve<MyChildView>().ShowDialog();
我想你可以在测试中以某种方式模拟对Unity容器的调用结果。或者,也许您可以在App类中编写诸如ShowMyChildView()之类的方法,它基本上只是做我上面描述的事情。这样调用App.ShowMyChildView()就很容易模拟,因为它只会返回一个bool?,对吧?
嗯,这可能并不比直接使用new MyChildView()更好,但这是我想到的一个小点子。我想分享一下。 =)

在我获得了一些经验之后,我发现Unity的使用是最好的。 - Budda
1
要小心,因为这是MVVM的违规操作,你正在ViewModel中引用视图(MyChildView)。不仅是视图类型,而且是直接使用实例(你调用ShowDialog())。 - Liero
也许我没有理解你的解决方案,因为在我的看法中,如果将它发送到 App 对象,那么子级的父级并不清晰。 - Rod

10

我有点晚了,但我觉得现有答案不够好。我会解释为什么。一般来说:

  • 从View中访问ViewModel是可以的,
  • 从ViewModel中访问View是错误的,因为它会引入循环依赖并使ViewModel难以测试。

Benny Jobigan的回答:

App.Container.Resolve<MyChildView>().ShowDialog();

这实际上并没有解决任何问题。你正在以一种紧密耦合的方式从ViewModel访问你的View。与new MyChildView().ShowDialog()唯一的区别是你使用了一层间接性。我没有看到直接调用MyChildView构造函数的任何优势。

如果你为视图使用接口,会更加清晰:

App.Container.Resolve<IMyChildView>().ShowDialog();`

现在ViewModel与视图没有紧密耦合。然而,我发现为每个视图创建接口相当不切实际。

arconaut的回答:

Messenger.Default.Send(new DialogMessage(...));

更好的方式是使用Messenger、EventAggregator或其他发布/订阅模式来解决MVVM中的所有问题 :) 缺点是它更难调试或导航到DialogMessageHandler,个人认为这太间接了。例如,你如何读取来自对话框的输出?通过修改DialogMessage吗?

我的解决方案:

您可以像这样从MainWindowViewModel打开窗口:

var childWindowViewModel = new MyChildWindowViewModel(); //you can set parameters here if necessary
var dialogResult = DialogService.ShowModal(childWindowViewModel);
if (dialogResult == true) {
   //you can read user input from childWindowViewModel
}

DialogService仅需要对话框的ViewModel,因此您的ViewModel与Views完全独立。运行时,DialogService可以找到适当的视图(例如使用命名约定)并显示它,或者在单元测试中可以轻松地模拟。

在我的情况下,我使用以下接口:

interface IDialogService
{
   void Show(IDialogViewModel dialog);
   void Close(IDialogViewModel dialog); 
   bool? ShowModal(IDialogViewModel dialog);
   MessageBoxResult ShowMessageBox(string message, string caption = null, MessageBoxImage icon = MessageBoxImage.No...);
}

interface IDialogViewModel 
{
    string Caption {get;}
    IEnumerable<DialogButton> Buttons {get;}
}

DialogButton指定DialogResult、ICommand或两者同时使用。


感谢您分享您的解决方案。您是否有办法将您的解决方案整合并与我分享?我正在尝试使用Visual Basic实现它,但遇到了一些问题。至少我可以确保我没有漏掉任何东西。 - Ehsan

2
请看一下我目前在Silverlight中显示模态对话框的MVVM解决方案。它解决了您提到的大部分问题,但完全抽象出了平台特定的东西,并且可以重复使用。此外,我没有使用任何代码后台,只是使用实现ICommand的DelegateCommands进行绑定。对话框基本上是一个视图-一个单独的控件,具有自己的ViewModel,并且从主屏幕的ViewModel显示,但通过DelagateCommand绑定从UI触发。
查看完整的Silverlight 4解决方案,请单击此处Modal dialogs with MVVM and Silverlight 4

1

我使用一个控制器来处理视图之间的所有信息传递。所有的视图模型都使用控制器中的方法来请求更多的信息,这些信息可以被实现为对话框、其他视图等。

它看起来像这样:

class MainViewModel {
    public MainViewModel(IView view, IModel model, IController controller) {
       mModel = model;
       mController = controller;
       mView = view;
       view.DataContext = this;
    }

    public ICommand ShowCommand = new DelegateCommand(o=> {
                  mResult = controller.GetSomeData(mSomeData);
                                                      });
}

class Controller : IController {
    public void OpenMainView() {
        IView view = new MainView();
        new MainViewModel(view, somemodel, this);
    }

    public int GetSomeData(object anyKindOfData) {
      ShowWindow wnd = new ShowWindow(anyKindOfData);
      bool? res = wnd.ShowDialog();
      ...
    }
}

谢谢你的回答,但你能否请澄清一下,在MainViewModel构造函数中,“view”变量是什么?你的ModelView中是否有对View的引用(这会破坏MVVM的设计思路)?另一个问题:你是如何通过Controller.OpenMainView打开窗口的?我看到你创建了“MainViewModel”的实例,但我没有看到创建view的地方...?谢谢。 - Budda
修复了缺失的引用。视图、模型和视图模型必须在某个地方创建并连接起来。例如,我是在控制器中完成的,但也可以在 IoC 容器中处理。你认为为什么从视图模型引用视图会破坏 MVVM 模式? - adrianm
2
感谢您的回答。请查看以下文章: http://msdn.microsoft.com/en-us/magazine/dd419663.aspx 与MVP中的Presenter不同,ViewModel不需要引用视图。这就是为什么我想避免从ModelView引用视图的原因。 - Budda
请注意,我没有引用真正的视图,而只是一个大多数情况下只包含DataContext和Close()的IView接口。如果我不在ViewModel中分配DataContext并且数据绑定Close没有意义,我认为使用IoC容器是不必要复杂的。 - adrianm
我只是覆盖了App.OnStartup()方法,在那里创建了我的主视图并将其传递给视图模型。我看了Jason Dolinger关于MVVM的视频,他就是这样做的。将接口引用(实际上仍然是引用,对吧?)传递给视图模型似乎不太好。 - Benny Jobigan

0

我的方法类似于adrianm的方法。但在我的情况下,控制器从不使用具体的视图类型。控制器完全解耦了视图 - 就像ViewModel一样。

这是如何工作的,可以看到WPF应用程序框架(WAF)的ViewModel示例。

.

此致敬礼,

jbe


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