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

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

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

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;

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


当前回答

一个好的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" />

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

其他回答

使用可冻结命令

<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();
    }
}

编辑: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

标准方法

在花了几年时间在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.

希望这对大家都有帮助。

我知道这是一个老问题,但当我做这个搜索时,我发现了很多相关的问题,但我没有找到一个真正明确的回答。所以我做了我自己的对话框/消息框/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 */ });

它展示了这样的东西:

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

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