在WPF的MVVM模式中,处理对话框是比较复杂的操作之一。由于视图模型不知道视图的任何信息,因此对话框通信可能会很有趣。我可以公开一个ICommand,当视图调用它时,就会出现一个对话框。

有人知道处理对话框结果的好方法吗?我说的是windows对话框,比如MessageBox。

其中一种方法是在视图模型上设置一个事件,当需要对话框时,视图会订阅该事件。

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;

这是可以的,但这意味着视图需要代码,这是我想要避免的。


我认为处理对话框应该是视图的责任,视图需要有代码来支持这一点。

如果你改变ViewModel - View交互来处理对话框,那么ViewModel依赖于该实现。处理这个问题最简单的方法是让视图负责执行任务。如果这意味着显示一个对话框,那么很好,但也可以是状态栏中的状态消息等。

我的观点是,MVVM模式的全部观点是将业务逻辑从GUI中分离出来,因此您不应该在业务层(ViewModel)中混合GUI逻辑(以显示对话框)。


在询问任务或对话框的视图模型应该是什么样子时,我也在思考一个类似的问题。

我目前的解决方案是这样的:

public class SelectionTaskModel<TChoosable> : ViewModel
    where TChoosable : ViewModel
{
    public SelectionTaskModel(ICollection<TChoosable> choices);
    public ReadOnlyCollection<TChoosable> Choices { get; }
    public void Choose(TChoosable choosen);
    public void Abort();
}

当视图模型决定需要用户输入时,它会调出一个SelectionTaskModel实例,其中包含用户可能的选择。基础设施负责调出相应的视图,该视图在适当的时候根据用户的选择调用Choose()函数。


我也遇到过同样的问题。我已经提出了一种在视图和视图模型之间进行交互的方法。您可以发起从ViewModel向View发送消息,告诉它显示一个消息框,然后它将报告返回结果。然后ViewModel可以响应从View返回的结果。

我在博客中对此进行了说明:


编辑:10多年后,我可以看出使用Mediator或任何其他消息传递模式在很多层面上都是一个非常糟糕的主意。不要这样做,只需实现Jeffrey的答案或在视图模型中注入一个IDialogService即可。


你应该找个中间人。 Mediator是一种常见的设计模式,在某些实现中也称为Messenger。 它是一种Register/Notify类型的范例,允许ViewModel和Views通过低耦合消息传递机制进行通信。

你应该看看谷歌WPF门徒组,然后搜索调解员。 你会很高兴得到答案的……

但是你可以这样开始:

http://joshsmithonwpf.wordpress.com/2009/04/06/a-mediator-prototype-for-wpf-apps/

享受吧!

编辑:你可以看到这个问题的答案与MVVM轻工具包在这里:

http://mvvmlight.codeplex.com/Thread/View.aspx?ThreadId=209338


一个好的MVVM对话框应该:

仅用XAML声明。 从数据绑定中获取它的所有行为。

不幸的是,WPF不提供这些特性。显示对话框需要对ShowDialog()进行代码隐藏调用。支持对话框的Window类不能在XAML中声明,因此它不能轻易地绑定到DataContext。

为了解决这个问题,我写了一个XAML存根控件,它位于逻辑树中,将数据绑定传递给一个Window,并处理显示和隐藏对话框的问题。你可以在这里找到它:http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

它使用起来非常简单,不需要对ViewModel进行任何奇怪的更改,也不需要事件或消息。基本调用如下所示:

<dialog:Dialog Content="{Binding Path=DialogViewModel}" Showing="True" />

您可能希望添加一个设置显示的样式。我在我的文章中解释过。我希望这对你有所帮助。


我认为视图可以有代码来处理来自视图模型的事件。

根据事件/场景的不同,它还可以具有订阅视图模型事件的事件触发器,以及响应中要调用的一个或多个操作。


我建议放弃20世纪90年代的模态对话框,而是实现一个覆盖控件(画布+绝对定位),可见性绑定到虚拟机中的布尔值。更接近ajax类型控件。

这非常有用:

<BooleanToVisibilityConverter x:Key="booltoVis" />

如:

<my:ErrorControl Visibility="{Binding Path=ThereWasAnError, Mode=TwoWay, Converter={StaticResource booltoVis}, UpdateSourceTrigger=PropertyChanged}"/>

下面是我如何实现一个用户控件。单击“x”关闭后面用户控件代码中的一行代码中的控件。(因为我有我的视图在一个。exe和ViewModels在一个dll,我不觉得代码操纵UI。)


我目前的解决方案解决了你提到的大部分问题,但它完全从特定于平台的东西中抽象出来,可以重用。 此外,我使用了没有代码隐藏,只与实现ICommand的DelegateCommands绑定。 对话框基本上是一个视图——一个独立的控件,它有自己的ViewModel,它从主屏幕的ViewModel显示,但通过DelagateCommand绑定从UI触发。

查看完整的Silverlight 4解决方案在这里与MVVM和Silverlight 4模态对话框


在学习(仍在学习)MVVM时,我真的为这个概念挣扎了一段时间。我的决定,以及我认为其他人已经决定但我不清楚的是:

我最初的想法是ViewModel不应该被允许直接调用对话框,因为它没有业务决定对话框应该如何显示。正因为如此,我开始思考如何像在MVP(即View.ShowSaveFileDialog())一样传递消息。然而,我认为这是错误的方法。

ViewModel直接调用对话框是可以的。然而,当您测试ViewModel时,这意味着对话框要么在测试期间弹出,要么全部失败(从未真正尝试过这个)。

因此,在测试时需要使用对话框的“测试”版本。这意味着,对于您拥有的任何对话,您都需要创建一个Interface,并模拟对话响应,或者创建一个具有默认行为的测试模拟。

您应该已经在使用某种服务定位器或IoC,您可以对其进行配置,以便根据上下文提供正确的版本。

使用这种方法,你的ViewModel仍然是可测试的,根据你如何模拟对话框,你可以控制行为。

希望这能有所帮助。


我遇到了同样的情况,并将MessageBox包装成一个设计器不可见控件。详情在我的博客里

http://geekswithblogs.net/mukapu/archive/2010/03/12/user-prompts-messagebox-with-mvvm.aspx

同样可以扩展到任何模态对话框,文件浏览控件等。


一个有趣的替代方法是使用控制器来显示视图(对话框)。

WPF应用程序框架(WAF)说明了这是如何工作的。


我在对这个问题的回答中描述了我自己的窗口加载器:

在应用程序中管理多个WPF视图


为什么不只是在VM中引发一个事件,然后在视图中订阅该事件呢?这将保持应用程序逻辑和视图分离,并且仍然允许您为对话框使用子窗口。


我已经实现了一个从ViewModel监听消息的行为。它基于Laurent Bugnion的解决方案,但由于它不使用代码,而且更可重用,所以我认为它更优雅。

如何使WPF的行为,如果MVVM是支持开箱即用


我在与MVVM的对话框中使用这种方法。

我现在要做的就是从我的视图模型中调用下面的。

var result = this.uiDialogService.ShowDialog("Dialogwindow title goes here", dialogwindowVM);

使用可冻结命令

<Grid>
        <Grid.DataContext>
            <WpfApplication1:ViewModel />
        </Grid.DataContext>


        <Button Content="Text">
            <Button.Command>
                <WpfApplication1:MessageBoxCommand YesCommand="{Binding MyViewModelCommand}" />
            </Button.Command>
        </Button>

</Grid>
public class MessageBoxCommand : Freezable, ICommand
{
    public static readonly DependencyProperty YesCommandProperty = DependencyProperty.Register(
        "YesCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty OKCommandProperty = DependencyProperty.Register(
        "OKCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty CancelCommandProperty = DependencyProperty.Register(
        "CancelCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty NoCommandProperty = DependencyProperty.Register(
        "NoCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
        "Message",
        typeof (string),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata("")
        );

    public static readonly DependencyProperty MessageBoxButtonsProperty = DependencyProperty.Register(
        "MessageBoxButtons",
        typeof(MessageBoxButton),
        typeof(MessageBoxCommand),
        new FrameworkPropertyMetadata(MessageBoxButton.OKCancel)
        );

    public ICommand YesCommand
    {
        get { return (ICommand) GetValue(YesCommandProperty); }
        set { SetValue(YesCommandProperty, value); }
    }

    public ICommand OKCommand
    {
        get { return (ICommand) GetValue(OKCommandProperty); }
        set { SetValue(OKCommandProperty, value); }
    }

    public ICommand CancelCommand
    {
        get { return (ICommand) GetValue(CancelCommandProperty); }
        set { SetValue(CancelCommandProperty, value); }
    }

    public ICommand NoCommand
    {
        get { return (ICommand) GetValue(NoCommandProperty); }
        set { SetValue(NoCommandProperty, value); }
    }

    public MessageBoxButton MessageBoxButtons
    {
        get { return (MessageBoxButton)GetValue(MessageBoxButtonsProperty); }
        set { SetValue(MessageBoxButtonsProperty, value); }
    }

    public string Message
    {
        get { return (string) GetValue(MessageProperty); }
        set { SetValue(MessageProperty, value); }
    }

    public void Execute(object parameter)
    {
        var messageBoxResult = MessageBox.Show(Message);
        switch (messageBoxResult)
        {
            case MessageBoxResult.OK:
                OKCommand.Execute(null);
                break;
            case MessageBoxResult.Yes:
                YesCommand.Execute(null);
                break;
            case MessageBoxResult.No:
                NoCommand.Execute(null);
                break;
            case MessageBoxResult.Cancel:
                if (CancelCommand != null) CancelCommand.Execute(null); //Cancel usually means do nothing ,so can be null
                break;

        }
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;


    protected override Freezable CreateInstanceCore()
    {
        throw new NotImplementedException();
    }
}

Karl Shifflett创建了一个使用服务方法和Prism InteractionRequest方法显示对话框的示例应用程序。

我喜欢这种服务方式——它不太灵活,所以用户不太可能弄坏东西:) 它也与我的应用程序的WinForms部分(MessageBox.Show)一致。 但如果您计划显示许多不同的对话框,那么InteractionRequest是更好的方法。

http://karlshifflett.wordpress.com/2010/11/07/in-the-box-ndash-mvvm-training/


编辑:是的,我同意这不是一个正确的MVVM方法,我现在使用类似于由blindmeis建议的东西。

其中一种方法是

在你的主视图模型中(打开模态的地方):

void OpenModal()
{
    ModalWindowViewModel mwvm = new ModalWindowViewModel();
    Window mw = new Window();
    mw.content = mwvm;
    mw.ShowDialog()
    if(mw.DialogResult == true)
    { 
        // Your Code, you can access property in mwvm if you need.
    }
}

在你的模态窗口View/ViewModel中:

XAML:

<Button Name="okButton" Command="{Binding OkCommand}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">OK</Button>
<Button Margin="2" VerticalAlignment="Center" Name="cancelButton" IsCancel="True">Cancel</Button>

ViewModel:

public ICommand OkCommand
{
    get
    {
        if (_okCommand == null)
        {
            _okCommand = new ActionCommand<Window>(DoOk, CanDoOk);
        }
        return _okCommand ;
    }
}

void DoOk(Window win)
{
    <!--Your Code-->
    win.DialogResult = true;
    win.Close();
}

bool CanDoOk(Window win) { return true; }

或者类似于这里发布的WPF MVVM:如何关闭一个窗口


有两种好方法可以做到这一点,1)对话框服务(简单,干净),2)视图辅助。视图辅助提供了一些简洁的特性,但通常不值得这样做。

对话框的服务

A)一个对话服务接口,比如via构造函数或一些依赖容器:

接口IDialogService { 任务ShowDialogAsync(DialogViewModel dlgVm); }

b) Your implementation of IDialogService should open a window (or inject some control into the active window), create a view corresponding to the name of the given dlgVm type (use container registration or convention or a ContentPresenter with type associated DataTemplates). ShowDialogAsync should create a TaskCompletionSource and return its .Task proptery. The DialogViewModel class itself needs an event you can invoke in the derived class when you want to close, and watch in the dialog view to actually close/hide the dialog and complete the TaskCompletionSource.

b)要使用,只需在某个dialogviewmodel派生类的实例上调用await this.DialogService.ShowDialog(myDlgVm)。等待返回后,查看你在对话框VM中添加的属性,以确定发生了什么;你甚至不需要回调。

查看帮助

这让你的视图监听视图模型上的事件。如果您愿意的话,可以将这些都打包到一个Blend Behavior中,以避免背后的代码和资源使用(FMI,子类化“Behavior”类,以查看类固醇上的一种可混合的附加属性)。现在,我们将手动在每个视图上执行此操作:

a)创建一个带有自定义有效负载(DialogViewModel派生类)的OpenXXXXXDialogEvent。

b)让视图在OnDataContextChanged事件中订阅该事件。如果旧值!= null并且在窗口的Unloaded事件中,请务必隐藏并取消订阅。

c)当事件触发时,让视图打开你的视图,它可能在你页面的资源中,或者你可以通过约定在其他地方定位它(比如在对话框服务方法中)。

这种方法更灵活,但需要做更多的工作才能使用。我不怎么用它。例如,一个很好的优点是能够将视图物理地放置在一个选项卡中。我已经使用一种算法将其放置在当前用户控件的边界中,或者如果不够大,则遍历可视树,直到找到足够大的容器。

这允许对话框靠近它们实际使用的地方,只使应用程序中与当前活动相关的部分变暗,并让用户在应用程序中移动而不必手动将对话框推开,甚至在不同的选项卡或子视图上打开多个准模态对话框。


我知道这是一个老问题,但当我做这个搜索时,我发现了很多相关的问题,但我没有找到一个真正明确的回答。所以我做了我自己的对话框/消息框/popin的实现,我分享它! 我认为这是“MVVM证明”,我试着让它简单和适当,但我是WPF的新手,所以请随意评论,甚至提出拉请求。

https://github.com/Plasma-Paris/Plasma.WpfUtils

你可以这样使用它:

public RelayCommand YesNoMessageBoxCommand { get; private set; }
async void YesNoMessageBox()
{
    var result = await _Service.ShowMessage("This is the content of the message box", "This is the title", System.Windows.MessageBoxButton.YesNo);
    if (result == System.Windows.MessageBoxResult.Yes)
        // [...]
}

或者像这样,如果你想要更复杂的popin:

var result = await _Service.ShowCustomMessageBox(new MyMessageBoxViewModel { /* What you want */ });

它展示了这样的东西:


关于这个主题,我已经写了一篇相当全面的文章,还为MVVM对话框开发了一个弹出式库。严格遵守MVVM不仅是可能的,而且在正确实现时非常干净,而且它可以很容易地扩展到自己不遵守MVVM的第三方库:

https://www.codeproject.com/Articles/820324/Implementing-Dialog-Boxes-in-MVVM


Sorry, but I have to chime in. I have been through several of the suggested solutions, before finding the Prism.Wpf.Interactivity namespace in the Prism project. You can use interaction requests and popup window action to either roll a custom window or for simpler needs there are built in Notification and Confirmation popups. These create true windows and are managed as such. you can pass a context object with any dependencies you need in the dialog. We use this solution at my work since I found it. We have numerous senior devs here and noone has come up with anything better. Our previous solution was the dialog service into an overlay and using a presenter class to make it happen, but you had to have factories for all of the dialog viewmodels, etc.

这不是小事,但也不是超级复杂。而且它是内置在Prism中,因此是最好(或更好)的实践。

我的2分钱!


标准方法

在花了几年时间在WPF中处理这个问题之后,我终于找到了在WPF中实现对话框的标准方法。以下是这种方法的优点:

清洁 不违反MVVM设计模式 ViewModal从不引用任何UI库(WindowBase, PresentationFramework等) 非常适合自动化测试 对话框可以很容易地被替换。

那么关键是什么呢?是DI + IoC。

下面是它的工作原理。我正在使用MVVM Light,但这种方法也可以扩展到其他框架:

Add a WPF Application project to your solution. Call it App. Add a ViewModal Class Library. Call it VM. App references VM project. VM project doesn't know anything about App. Add NuGet reference to MVVM Light to both projects. I'm using MVVM Light Standard these days, but you are okay with the full Framework version too. Add an interface IDialogService to VM project: public interface IDialogService { void ShowMessage(string msg, bool isError); bool AskBooleanQuestion(string msg); string AskStringQuestion(string msg, string default_value); string ShowOpen(string filter, string initDir = "", string title = ""); string ShowSave(string filter, string initDir = "", string title = "", string fileName = ""); string ShowFolder(string initDir = ""); bool ShowSettings(); } Expose a public static property of IDialogService type in your ViewModelLocator, but leave registration part for the View layer to perform. This is the key.: public static IDialogService DialogService => SimpleIoc.Default.GetInstance<IDialogService>(); Add an implementation of this interface in the App project. public class DialogPresenter : IDialogService { private static OpenFileDialog dlgOpen = new OpenFileDialog(); private static SaveFileDialog dlgSave = new SaveFileDialog(); private static FolderBrowserDialog dlgFolder = new FolderBrowserDialog(); /// <summary> /// Displays a simple Information or Error message to the user. /// </summary> /// <param name="msg">String text that is to be displayed in the MessageBox</param> /// <param name="isError">If true, Error icon is displayed. If false, Information icon is displayed.</param> public void ShowMessage(string msg, bool isError) { if(isError) System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Error); else System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Information); } /// <summary> /// Displays a Yes/No MessageBox.Returns true if user clicks Yes, otherwise false. /// </summary> /// <param name="msg"></param> /// <returns></returns> public bool AskBooleanQuestion(string msg) { var Result = System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes; return Result; } /// <summary> /// Displays Save dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if /// user clicks Save button. Returns null if user clicks Cancel button. /// </summary> /// <param name="filter"></param> /// <param name="initDir"></param> /// <param name="title"></param> /// <param name="fileName"></param> /// <returns></returns> public string ShowSave(string filter, string initDir = "", string title = "", string fileName = "") { if (!string.IsNullOrEmpty(title)) dlgSave.Title = title; else dlgSave.Title = "Save"; if (!string.IsNullOrEmpty(fileName)) dlgSave.FileName = fileName; else dlgSave.FileName = ""; dlgSave.Filter = filter; if (!string.IsNullOrEmpty(initDir)) dlgSave.InitialDirectory = initDir; if (dlgSave.ShowDialog() == DialogResult.OK) return dlgSave.FileName; else return null; } public string ShowFolder(string initDir = "") { if (!string.IsNullOrEmpty(initDir)) dlgFolder.SelectedPath = initDir; if (dlgFolder.ShowDialog() == DialogResult.OK) return dlgFolder.SelectedPath; else return null; } /// <summary> /// Displays Open dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if /// user clicks Open button. Returns null if user clicks Cancel button. /// </summary> /// <param name="filter"></param> /// <param name="initDir"></param> /// <param name="title"></param> /// <returns></returns> public string ShowOpen(string filter, string initDir = "", string title = "") { if (!string.IsNullOrEmpty(title)) dlgOpen.Title = title; else dlgOpen.Title = "Open"; dlgOpen.Multiselect = false; dlgOpen.Filter = filter; if (!string.IsNullOrEmpty(initDir)) dlgOpen.InitialDirectory = initDir; if (dlgOpen.ShowDialog() == DialogResult.OK) return dlgOpen.FileName; else return null; } /// <summary> /// Shows Settings dialog. /// </summary> /// <returns>true if User clicks OK button, otherwise false.</returns> public bool ShowSettings() { var w = new SettingsWindow(); MakeChild(w); //Show this dialog as child of Microsoft Word window. var Result = w.ShowDialog().Value; return Result; } /// <summary> /// Prompts user for a single value input. First parameter specifies the message to be displayed in the dialog /// and the second string specifies the default value to be displayed in the input box. /// </summary> /// <param name="m"></param> public string AskStringQuestion(string msg, string default_value) { string Result = null; InputBox w = new InputBox(); MakeChild(w); if (w.ShowDialog(msg, default_value).Value) Result = w.Value; return Result; } /// <summary> /// Sets Word window as parent of the specified window. /// </summary> /// <param name="w"></param> private static void MakeChild(System.Windows.Window w) { IntPtr HWND = Process.GetCurrentProcess().MainWindowHandle; var helper = new WindowInteropHelper(w) { Owner = HWND }; } } While some of these functions are generic (ShowMessage, AskBooleanQuestion etc.), others are specific to this project and use custom Windows. You can add more custom windows in the same fashion. The key is to keep UI-specific elements in the View layer and just expose the returned data using POCOs in the VM layer. Perform IoC Registration your interface in the View layer using this class. You can do this in your main view's constructor (after InitializeComponent() call): SimpleIoc.Default.Register<IDialogService, DialogPresenter>(); There you go. You now have access to all your dialog functionality at both VM and View layers. Your VM layer can call these functions like this: var NoTrump = ViewModelLocator.DialogService.AskBooleanQuestion("Really stop the trade war???", ""); So clean you see. The VM layer doesn't know nothing about how a Yes/No question will be presented to the user by the UI layer and can still successfully work with the returned result from the dialog.

其他免费福利

For writing unit test, you can provide a custom implementation of IDialogService in your Test project and register that class in IoC in the constructor your test class. You'll need to import some namespaces such as Microsoft.Win32 to access Open and Save dialogs. I have left them out because there is also a WinForms version of these dialogs available, plus someone might want to create their own version. Also note that some of the identifier used in DialogPresenter are names of my own windows (e.g. SettingsWindow). You'll need to either remove them from both the interface and implementation or provide your own windows. If your VM performs multi-threading, call MVVM Light's DispatcherHelper.Initialize() early in your application's life cycle. Except for DialogPresenter which is injected in the View layer, other ViewModals should be registered in ViewModelLocator and then a public static property of that type should be exposed for the View layer to consume. Something like this: public static SettingsVM Settings => SimpleIoc.Default.GetInstance<SettingsVM>(); For the most part, your dialogs should not have any code-behind for stuff like binding or setting DataContext etc. You shouldn't even pass things as constructor parameters. XAML can do that all for you, like this: <Window x:Class="YourViewNamespace.SettingsWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:local="clr-namespace:YourViewProject" xmlns:vm="clr-namespace:YourVMProject;assembly=YourVMProject" DataContext="{x:Static vm:ViewModelLocator.Settings}" d:DataContext="{d:DesignInstance Type=vm:SettingsVM}" /> Setting DataContext this way gives you all kinds of design-time benefits such as Intellisense and auto-completion.

希望这对大家都有帮助。


最简单的方法:使用HanumanInstitute。MvvmDialogs图书馆

如果您遵循文档,您可以像这样漂亮地使用它,并与UI完全解耦

var presetName = await dialogService.ShowSavePresetViewAsync(this);