WPF从ViewModel打开新视图

9
这是我的第一个WPF-MVVM应用程序,以下是我的结构:
1. 一个项目包括app.xaml,用于打开应用程序并重写OnStartup以解决MainWindow(由于引用而这样做); 2. 一个项目用于我的Views; 3. 一个项目用于我的ViewModels; 4. 一个项目用于我的Model。
我有以下问题:当我在MainWindowView上点击按钮以显示另一个视图时,我应该如何从我的MainWindowViewModel中打开另一个视图,而我的View项目已经引用了ViewModel项目,我不能将ViewModel项目与View项目进行引用。顺便说一下,我正在使用Unity进行依赖注入。
所以,你能帮我吗?

不错的问题。不久前我也问了类似的问题:http://stackoverflow.com/questions/13449750/who-is-responsible-for-window-view-lifecylce-in-mvvm-pattern 就我所知,关于这个问题没有严格的规定。 - Pavel Voronin
@voroninp 我认为应该存在一些推荐的结构来使用MVVM。 - Guilherme Oliveira
3个回答

11

有几种方法可以实现。

您可以定义一个对话框/导航/窗口服务接口,该接口在ViewModels项目中定义。您需要决定ViewModels将如何表达它们想要打开哪个窗口。我通常使用IDialogViewModel接口,其中一些ViewModel实现,并将ViewModel的实例传递到服务中,但您可以使用枚举、字符串等任何你想要的方式,以便您的实现可以映射到将要打开的真实窗口。

例如:

public interface IDialogService
{
    bool? ShowDialog(object dialogViewModel); 
}

希望打开新窗口的视图模型将接收该服务的实例,并使用它来表达打开窗口的意图。在您的视图项目中,您需要定义一个类型来实现您的服务接口,并将打开窗口的实际逻辑放在其中。

以此示例为例:

public class DialogService : IDialogService
{
    private Stack<Window> windowStack = new Stack<Window>();


    public DialogService(Window root)
    {
        this.windowStack.Push(root);
    }

    public bool? ShowDialog(object dialogViewModel)
    {
        Window dialog = MapWindow(dialogViewModel); 
        dialog.DataContext = dialogViewModel;
        dialog.Owner = this.windowStack.Peek();

        this.windowStack.Push(dialog);

        bool? result;

        try
        {
            result = dialog.ShowDialog();
        }
        finally
        {
            this.windowStack.Pop();
        }

        return result;
    }

}

您的主要项目将负责创建和注入对话服务到需要它的ViewModels中。在示例中,应用程序将创建一个新的对话服务实例,并将MainWindow传递给它。

另一种实现方式是使用某种形式的消息模式(链接1 链接2)。此外,如果您想要简单的方法,还可以使您的ViewModels在需要打开Windows时引发事件,并让Views订阅这些事件。

编辑

我在我的应用程序中使用的完整解决方案通常会更加复杂,但基本思路是相同的。我有一个基准的DialogWindow,它期望一个实现了IDialogViewModel接口的ViewModel作为DataContext。此接口抽象了您在对话框中期望的一些功能,例如接受/取消命令以及关闭事件,因此您也可以从ViewModel中关闭窗口。DialogWindow基本上由ContentPresenter组成,其Content属性绑定到DataContext,并在DataContext更改时挂钩关闭事件(以及其他一些内容)。

每个“对话框”由一个IDialogViewModel和一个关联的View(UserControl)组成。为了映射它们,我只需在应用程序的资源中声明隐式DataTemplates。在我展示的代码中,唯一的区别是不会有MapWindow方法,窗口实例总是DialogWindow。

我使用一种额外的技巧来在对话框之间重复使用布局元素。一种方法是将它们包含在DialogWindow中(接受/取消按钮等)。我喜欢保持DialogWindow的整洁(因此我甚至可以用它来创建“非对话框”对话框)。我为ContentControl声明了一个包含公共接口元素的模板,当我声明一个View-ViewModel映射模板时,我使用带有我的“对话框模板”的ContentControl包装View。然后,您可以为DialogWindow拥有尽可能多的“Master templates”,如“向导样式”(例如)。


我有这个IDialogService,但我只是用它来显示异常或成功的消息。我正在考虑改变我的结构,使其更易理解,并且我也会为这个接口提供更多的可用性。我很喜欢你的答案,因为我已经考虑过了。一旦我完成了,我会告诉你我做了什么,好吗?谢谢。 - Guilherme Oliveira
根据我在我的应用程序中使用的设置,添加了一些提示,希望能有所帮助。 - Arthur Nunes
我改变了我的解决方案,现在View和ViewModel在同一个项目中,我从ViewModel调用IDialogService来打开View。因此,我将打开视图的责任放在接口上。感谢您的帮助。 - Guilherme Oliveira
@ArthurNunes 感谢您的解释。但是,当涉及到实现您的想法时,很难完成。您能否分享一个非常简单的项目来实现这个想法?我几乎完成了我的项目的所有其他部分,但我无法通过服务打开、关闭和发送ViewModel。您能帮忙吗? - Ehsan

3

简单直接的方法

如果我理解正确,MainWindowView在应用程序启动时通过Unity解析,解析其对MainWindowViewModel的依赖关系?

如果您正在使用这种流程,我建议继续沿着同样的路径,并让MainWindowView通过按钮的简单点击处理程序处理打开新视图的操作。在此处理程序中,您可以解析新视图,这将解析该视图的视图模型,然后您也会回到新视图的MVVM领域。

这种解决方案很简单,对于大多数较小的应用程序来说完全可以正常工作。

更复杂的应用程序需要更重的方法

如果您不想使用这种视图优先的流程,我建议引入某种控制器/Presenter来协调视图和视图模型。 Presenter负责决定实际何时打开/关闭视图等。

虽然这是一种相当重的抽象,更适合于更复杂的应用程序,但请确保您真正从中获得足够的收益,以证明添加的抽象/复杂性是有价值的。

以下是此方法可能看起来像的代码示例:

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        var container = new UnityContainer();

        container.RegisterType<IMainView, MainWindow>();
        container.RegisterType<ISecondView, SecondWindow>();
        container.RegisterType<IMainPresenter, MainPresenter>();
        container.RegisterType<ISecondPresenter, SecondPresenter>();

        var presenter = container.Resolve<IMainPresenter>();

        presenter.ShowView();
    }
}

public interface IMainPresenter
{
    void ShowView();
    void OpenSecondView();
}

public interface ISecondPresenter
{
    void ShowView();
}

public interface ISecondView
{
    void Show();
    SecondViewModel ViewModel { get; set; }
}

public interface IMainView
{
    void Show();
    MainViewModel ViewModel { get; set; }
}

public class MainPresenter : IMainPresenter
{
    private readonly IMainView _mainView;
    private readonly ISecondPresenter _secondPresenter;

    public MainPresenter(IMainView mainView, ISecondPresenter secondPresenter)
    {
        _mainView = mainView;
        _secondPresenter = secondPresenter;
    }

    public void ShowView()
    {
        // Could be resolved through Unity just as well
        _mainView.ViewModel = new MainViewModel(this);

        _mainView.Show();
    }

    public void OpenSecondView()
    {
        _secondPresenter.ShowView();
    }
}

public class SecondPresenter : ISecondPresenter
{
    private readonly ISecondView _secondView;

    public SecondPresenter(ISecondView secondView)
    {
        _secondView = secondView;
    }

    public void ShowView()
    {
        // Could be resolved through Unity just as well
        _secondView.ViewModel = new SecondViewModel();
        _secondView.Show();
    }
}

public class MainViewModel
{
    public MainViewModel(MainPresenter mainPresenter)
    {
        OpenSecondViewCommand = new DelegateCommand(mainPresenter.OpenSecondView);
    }

    public DelegateCommand OpenSecondViewCommand { get; set; }
}

public class SecondViewModel
{
}

<!-- MainWindow.xaml -->
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Button Command="{Binding OpenSecondViewCommand}" Content="Open second view" />
    </Grid>
</Window>

<!-- SecondWindow.xaml -->
<Window x:Class="WpfApplication1.SecondWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="SecondWindow" Height="350" Width="525">
    <Grid>
        <TextBlock>Second view</TextBlock>
    </Grid>
</Window>

本文提供了一种类似于我之前在生产中使用的解决方案。


是的,我的MainWindowsView是通过Unity解析的。我认为最好的方法是始终使用Unity来解析其他视图。我会尝试按照你说的去做。 - Guilherme Oliveira

0
要从MainWindowView打开新窗口,您需要将Frame组件或整个窗口的引用传递给MainWindowViewModel对象(当绑定命令到转换按钮或其他内容时,将它们作为对象传递),在那里您可以导航到新页面,但是,如果在过渡时没有特殊的事情需要在ViewModel中完成,您可以只使用经典的ButtonClick事件或MainWindowView.cs中的其他事件来进行导航,这对于基本的过渡是可以的。
附言:我不确定为什么要为ViewModels/Views/Models使用不同的项目。

我会尝试按照你说的去做。我正在使用不同的项目,因为MVVM有不同的抽象层级。我不知道这是否是正确的方式。 - Guilherme Oliveira
其实,我正在重新考虑将我的项目更改为一个新的结构,所有内容都在同一个项目中,因为我认为当前的结构以后会让我头疼哈哈。 - Guilherme Oliveira

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