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

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

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

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;

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


当前回答

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分钱!

其他回答

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

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

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

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

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

希望这对大家都有帮助。

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

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

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