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

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

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

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;

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


当前回答

标准方法

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

希望这对大家都有帮助。

其他回答

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

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

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

使用可冻结命令

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

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

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

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()函数。

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

编辑:是的,我同意这不是一个正确的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:如何关闭一个窗口