在MVVM WPF中打开新窗口

59

我有一个按钮,我将这个按钮绑定到ViewModel中的一个命令,例如OpenWindowCommand。当我点击按钮时,我想要打开一个新窗口。但是从ViewModel中创建窗口实例并显示窗口违反了MVVM。我已经创建了接口,如下:

interface IWindowService
{
    void showWindow(object dataContext);
}

并且WindowService实现了这个接口,就像这样

class WindowService : IWindowService
{
    public void showWindow(object dataContext)
    {
        ChildWindow window=new ChildWindow();
        window.DataContext=dataContext;
        window.Show();
    }
}

在这个类中,我已经指定了ChildWindow。因此,这个类与显示ChildWindow紧密耦合。当我想要显示另一个窗口时,我必须实现另一个具有相同接口和逻辑的类。如何使这个类通用化,以便我只需传递任何窗口的实例,该类就能打开任何窗口?

我没有使用任何内置的MVVM框架。我已经阅读了许多StackOverflow上的文章,但是我没有找到任何解决方案。


2
我发现了一种打开MVVM中窗口的替代方法,使用行为而不是服务。 - Mike Fuchs
7个回答

63

你说“从视图模型中创建窗口实例并显示窗口是违反MVVM的”。这是正确的。

现在,您正在尝试创建一个接口,该接口接受由VM指定的视图类型。这同样是一种违规行为。您可能已经通过接口抽象了创建逻辑,但仍然在VM内部请求视图创建。

VM只应关心创建VM。如果您确实需要一个新窗口来托管新VM,则提供一个与您所做的类似的接口,但不要使用视图。您为什么需要这个视图?大多数(VM first)MVVM项目使用隐式数据模板将视图与特定的VM相关联。VM对它们一无所知。

像这样:

class WindowService:IWindowService
{
    public void ShowWindow(object viewModel)
    {
        var win = new Window();
        win.Content = viewModel;
        win.Show();
    }
}
很明显,你需要确保在app.xaml中设置了VM->View隐式模板才能使其工作。这只是标准的VM first MVVM。
例如:

很明显,你需要确保在app.xaml中设置了VM->View隐式模板才能使其工作。这只是标准的VM first MVVM。

<Application x:Class="My.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="clr-namespace:My.App.ViewModels"
             xmlns:vw="clr-namespace:My.App.Views"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

        <DataTemplate DataType="{x:Type vm:MyVM}">
            <vw:MyView/>
        </DataTemplate>

    </Application.Resources>
</Application>

3
为什么需要不同类型的窗口?窗口只是视图的容器。只需使用通用窗口,并像正常情况下映射VM->View那样使用隐式数据模板即可。 - GazTheDestroyer
3
VM不了解View的原因之一是因为您可以使用多个View以不同的方式显示ViewModel中的数据。这种方法使得您的视图和视图模型具有1:1的映射关系。 - Nick
12
如果MyView是一个窗口,那么这个解决方案将不起作用。它会抛出一个“无法将窗口放置在样式中”的错误。 - Jack Frost
10
我希望 vw:MyView 应该是 UserControl 类型而不是 Window 类型。 - Gopichandar
2
我知道这是一个晚评论,但我同意@Gopichandar的观点。因为当我将Window添加到Content中时,它会抛出一个运行时异常:“Window必须是树的根。不能将Window作为Visual的子级添加”。 - Sats
显示剩余22条评论

7

一种可能的解决方案是:

class WindowService:IWindowService
{
 public void showWindow<T>(object DataContext) where T: Window, new() 
 {
  ChildWindow window=new T();
  window.Datacontext=DataContext;
  window.show();
 }
}

然后你可以这样做:
windowService.showWindow<Window3>(windowThreeDataContext);

有关新约束的更多信息,请参见http://msdn.microsoft.com/en-gb/library/sd2w2ew5.aspx

注意:new()约束仅适用于具有无参数构造函数的窗口(但我想这在此情况下不应该是问题!)在更一般的情况下,请参见Create instance of generic type?以供参考。


11
windowService.showWindow<Window3>(windowThreeDataContext); 这个语句出现在ViewModel中,并包含视图名称。它违反了MVVM模式吗? 这个语句可能违反MVVM模式,因为ViewModel应该与View完全独立,并且不应该直接引用View的名称。应该使用数据绑定和命令来处理View和ViewModel之间的通信。但是,具体情况取决于整个应用程序的架构和实现。 - DT sawant
确实抱歉,我对“如何使此类通用,以便只需传递任何窗口的实例,类将能够打开任何窗口?”这个问题的回答有点过于强烈了,并没有正确地讨论根本问题!在 MVVM 方法中,您需要创建窗口/视图,因此上面的解决方案可能会有所帮助-您可以将 ViewModel 映射到 View,或者使用某种形式的约定(例如 \ViewModels\MyViewModel.cs->\Views\MyView.cs),但这取决于您 :) - David E
1
个人而言,如果你想采用严格的MVVM方法,我会建议使用框架,因为它通常会为你封装所有这些 :)。我使用过 Caliburn Micro,我真的很喜欢,但这取决于你 ^^ - David E

4

You could write a function like this:

class ViewManager
{
    void ShowView<T>(ViewModelBase viewModel)
        where T : ViewBase, new()
    {
        T view = new T();
        view.DataContext = viewModel;
        view.Show(); // or something similar
    }
}

abstract class ViewModelBase
{
    public void ShowView(string viewName, object viewModel)
    {
        MessageBus.Post(
            new Message 
            {
                Action = "ShowView",
                ViewName = viewName,
                ViewModel = viewModel 
            });
    }
}

确保ViewBase具有DataContext属性。(您可以继承UserControl)

一般来说,我会创建一种消息总线,并让ViewManager监听请求视图的消息。ViewModel将发送一个消息,要求显示视图和要显示的数据。然后,ViewManager将使用上述代码。

为了防止调用ViewModel了解View类型,您可以将视图的字符串/逻辑名称传递给ViewManager,并让ViewManager将逻辑名称转换为类型。


我可能错了,但我相当确定你需要在函数中使用where T: ViewBase, new()才能创建一个新的泛型类型对象?就像这样:http://msdn.microsoft.com/en-gb/library/sd2w2ew5.aspx - David E
这是一个同时回答的情况 - 当我写我的回答时,你的回答不在那里,然后我刷新页面发现你已经抢先一步了!抱歉 Erno :) - David E
@DavidEdey - :) 没问题,这不是一个竞赛。 - Emond
1
@ErnodeWeerd 在你的情况下,我仍然需要从ViewModel中引用View。我不必在那里创建实例,但至少我必须引用View。这样做是否违反了MVVM? - DT sawant
@DTsawant - 不需要。请阅读我回答的最后一部分。 - Emond
我还添加了发送消息的功能。不过要注意,如果你继续这样做,最终可能会写出一个已经存在的MVVM库 :) - Emond

3

使用ContentPresenter可以在窗口中绑定您的DataConext,然后为您的DataContext定义一个DataTemplate,以便WPF可以呈现您的DataContext。类似于我的DialogWindow服务

所以你只需要一个包含ContentPresenter的ChildWindow:

<Window x:Class="ChildWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
<ContentPresenter Content="{Binding .}">

</ContentPresenter>
</Window>

2

我发现这个被接受的解决方案非常有用,但在实际使用中,我发现它缺乏使用户控件(从VM映射到View的视图)停靠在托管窗口内以占用其提供的整个区域的能力。因此,我扩展了该解决方案,以包括此功能:

public Window CreateWindowHostingViewModel(object viewModel, bool sizeToContent)
{
   ContentControl contentUI = new ContentControl();
   contentUI.Content = viewModel;
   DockPanel dockPanel = new DockPanel();
   dockPanel.Children.Add(contentUI);
   Window hostWindow = new Window();
   hostWindow.Content = dockPanel;

   if (sizeToContent)
       hostWindow.SizeToContent = SizeToContent.WidthAndHeight;

   return hostWindow;
}

这里的技巧是使用DockPanel来托管从VM转换而来的视图。
然后,如果您希望窗口的大小与其内容的大小匹配,请按以下方式使用先前的方法:
var win = CreateWindowHostingViewModel(true, viewModel)
win.Title = "Window Title";
win.Show();

如果您的窗口尺寸固定,则可以按照以下方式进行操作:

var win = CreateWindowHostingViewModel(false, viewModel)
win.Title = "Window Title";
win.Width = 500;
win.Height = 300;
win.Show();

0

或许你可以传递窗口类型。

尝试使用Activator.CreateInstance()

请参考以下问题: 在运行时确定类型实例化对象

chakrit的解决方案:

// determine type here
var type = typeof(MyClass);

// create an object of the type
var obj = (MyClass)Activator.CreateInstance(type);

0

这里是一点贡献。为了更加通用,我们可以获取传递给showWindow方法的viewModel类型,然后查找其对应的视图,最后实例化它。

class WindowService : IWindowService
    {
        public void showWindow(object viewModel)
        {
            string viewToSearch;
            Type foundViewType;


            // Find the type of the viewModel
            Type t = viewModel.GetType();

            // Get the views            
            List<Type> myViews = Assembly.GetExecutingAssembly().GetTypes()
                      .Where(t => t.Namespace == "[yourNameSpace].Views")
                      .ToList();

            // Find the corresponding view
            viewToSearch = t.Name.Replace("ViewModel", "View");
            foundViewType = myViews.Find(x => x.Name.Equals(viewToSearch));

            if (foundViewType != null)
            {
                var window = Activator.CreateInstance(foundViewType);

                ((Window)window).DataContext = viewModel;
                ((Window)window).Show();
            }

        }
    }

然后你可以在你的viewModel中调用它

WindowService windowService = new WindowService();
windowService.showWindow(this);

欢迎来到StackOverflow!请确保您用英语回答问题,包括代码和注释。 - Batesias

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