用MVVM处理WPF中的对话框

在WPF的MVVM模式中,处理对话框是更复杂的操作之一。 由于您的视图模型对视图没有任何了解,因此对话交stream可能很有趣。 我可以公开一个ICommand,当视图调用它时,会出现一个对话框。

有谁知道处理对话结果的好方法吗? 我正在谈论诸如MessageBox之类的Windows对话框。

我们做这件事的方法之一就是在视图模型中有一个事件,当需要对话时视图将会订阅。

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog; 

这是好的,但这意味着该视图需要代码,这是我想远离的东西。

我build议放弃20世纪90年代的模式对话框,而是将控制作为覆盖(canvas+绝对定位)实现,并将可见性绑定到虚拟机中的布尔值。 接近ajaxtypes的控制。

这非常有用:

 <BooleanToVisibilityConverter x:Key="booltoVis" /> 

如下所示:

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

以下是我作为用户控件实现的一个方法。 点击'x'closures后面的usercontrol代码中的一行代码中的控件。 (因为我有一个DLL中的.exe和ViewModels中的我的视图,我不觉得操纵UI的代码感觉不好。)

Wpf对话框

你应该为此使用中介。 介体是一种常见的devise模式,在其一些实现中也被称为Messenger 。 这是一个Register / Notifytypes的范例,可以使ViewModel和Views通过低耦合的消息机制进行通信。

你应该看看谷歌WPF门徒小组,只是search调解人。 你会很高兴的答案…

你可以从这开始:

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

请享用 !

编辑:你可以在这里看到MVVM Light Toolkit的这个问题的答案:

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

一个好的MVVM对话框应该:

  1. 只用XAML声明。
  2. 从数据绑定中获取所有的行为。

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

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

这只是简单地使用,并不需要任何奇怪的变化你的ViewModel,并不需要事件或消息。 基本的调用如下所示:

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

您可能想要添加设置显示的样式。 我在我的文章中解释。 我希望这可以帮助你。

我使用这种方法与MVVM进行对话。

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

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

我目前的解决scheme解决了您提到的大多数问题,但是完全从平台特定的东西中抽象出来,可以重复使用。 我也没有使用code-behind绑定与实现ICommand的DelegateCommands。 对话框基本上是一个视图 – 一个单独的控件,它有自己的ViewModel,它显示在主屏幕的ViewModel中,但是通过DelawareCommand绑定从UI触发。

在这里看到完整的Silverlight 4解决schemeMVVM和Silverlight 4的模式对话框

在学习(还在学习)MVVM时,我真的很困扰这个概念。 我决定了什么,我认为别人已经决定了,但是我不明白的是:

我原来的想法是,ViewModel不应该被允许直接调用对话框,因为它没有决定如何显示一个对话框的业务。 因为这个,我开始考虑如何传递消息,就像我在MVP(即View.ShowSaveFileDialog())中那样。 不过,我认为这是错误的做法。

ViewModel可以直接调用对话框。 但是,当你testingViewModel的时候,这意味着对话框会在你的testing中popup,或者一起失败(从来没有真正尝试过)。

所以,需要做的是在testing中使用对话框的“testing”版本。 这意味着,对于任何对话,你需要创build一个接口,或者模拟出对话框响应,或者创build一个默认行为的testing模拟。

您应该已经使用某种可以configuration的服务定位器或IoC,以根据上下文为您提供正确的版本。

使用这种方法,您的ViewModel仍然是可testing的,取决于您如何模拟对话框,您可以控制行为。

希望这可以帮助。

使用一个freezable命令

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

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

对话服务

a)像通过构造函数或某个依赖容器一样的对话服务接口:

interface IDialogService { Task ShowDialogAsync(DialogViewModel dlgVm); }

b)你的IDialogService的实现应该打开一个窗口(或者注入一些控制到活动窗口中),创build一个对应于给定的dlgVmtypes的名称的视图(使用容器注册或者约定或者带有关联DataTemplates的ContentPresenter)。 ShowDialogAsync应该创build一个TaskCompletionSource并返回它的.Task声明。 DialogViewModel类本身需要一个你可以在closures的时候在派生类中调用的事件,并且在对话视图中观察实际closures/隐藏对话框并完成TaskCompletionSource。

b)若要使用,只需在您的DialogViewModel派生类的实例上调用await this.DialogService.ShowDialog(myDlgVm)即可。 等待返回后,查看您添加到对话虚拟机上的属性,以确定发生了什么; 你甚至不需要callback。

查看帮助

这让你的视图监听视图模型上的事件。 这可以全部包装成一个混合行为,以避免代码隐藏和资源使用,如果你这么倾向(FMI,子类的“行为”类看到类固醇的一种Blendable附加属性)。 现在,我们将在每个视图上手动执行此操作:

a)用自定义有效载荷(DialogViewModel派生类)创build一个OpenXXXXXDialogEvent。

b)让视图在其OnDataContextChanged事件中订阅事件。 一定要隐藏和取消订阅,如果旧的值!= null,并在窗口的卸载事件。

c)当事件触发时,让视图打开你的视图,这个视图可能位于你的页面的一个资源上,或者你可以按照其他地方的惯例来定位它(比如在对话框服务方法中)。

这种方法更加灵活,但需要更多的工作才能使用。 我用不了多less。 例如,一个很好的优点是能够将视图物理放置在标签内。 我使用了一种algorithm将其放置在当前用户控件的边界内,或者如果不够大,则遍历可视化树直到find足够大的容器。

这允许对话框接近实际使用的位置,只调整与当前活动相关的应用程序的部分,并让用户在应用程序内移动,而不必手动推开对话框,甚至有多个准静态对话框,modal dialog在不同的选项卡或子视图上打开。

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

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

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

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

我的观点是,MVVM模式的要点是将业务逻辑与GUI分开,所以不应该在业务层(ViewModel)中混合GUI逻辑(显示对话框)。

我有相同的情况,并将MessageBox封装到一个devise器不可见的控件中。 细节在我的博客

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

同样可以扩展到任何modal dialog,文件浏览控件等

一个有趣的select是使用负责显示视图(对话框)的控制器。

WPF应用程序框架(WAF)显示了如何工作。

为什么不在虚拟机中引发事件并在视图中订阅事件呢? 这将保持应用程序逻辑和视图独立,并仍然允许您使用子窗口进行对话。

花了一些时间后,我终于想出了以下解决scheme。 这种方法的几个关键优势是:

  1. 实现MVVM Light自己的IDialogService
  2. View不需要添加MVVM Light的参考。
  3. VM不需要执行任何演示级别的活动。 甚至不需要PresentationFramework参考。
  4. 使用MVVM Light自己的Messenger通道,所以表示层和VM层是分离的。
  5. 支持带有返回值的对话框,例如是/否问题或确定/取消情况。
  6. 支持自定义对话框。

这里是IDialogService的实现(进入ViewModel项目):

 using System; using System.Linq; using System.Threading.Tasks; namespace VM { public enum MessageBoxButtonVM { OK, OKCancel, YesNo } public enum MessageBoxImageVM { None, Information, Question, Error } public class MessageBoxArgs { public MessageBoxButtonVM Buttons { get; set; } public MessageBoxImageVM Icon { get; set; } public string Title { get; set; } public string Message { get; set; } } //For custom dialogs that return a value public class MessageBoxNotificationWithAction<T> { private readonly Action<T> _callback; public MessageBoxArgs Notification { get; set; } public MessageBoxNotificationWithAction(MessageBoxArgs notification, Action<T> callback) { Notification = notification; CheckCallback(callback); _callback = callback; } public virtual void Execute(T argument) { _callback.Invoke(argument); } private static void CheckCallback(Delegate callback) { if (callback == null) { throw new ArgumentNullException(nameof(callback), "Callback must not be null"); } } } /// <summary> /// Provides an implementation-agnostic way of communicating with the user through dialog boxes. Clients must register for communication messages using /// MVVM Light messaging system. /// </summary> public class DialogService : GalaSoft.MvvmLight.Views.IDialogService { private static GalaSoft.MvvmLight.Messaging.IMessenger Messenger = GalaSoft.MvvmLight.Messaging.Messenger.Default; private string _ProductName = ""; public string ProductName { get { if (_ProductName == "") { //The following statement returns the Title attribute of the current assembly, as defined in project properties (Assembly Information dialog). var TitleAttrib = System.Reflection.Assembly.GetExecutingAssembly().GetCustomAttributesData().First(x => x.AttributeType.Name == "AssemblyTitleAttribute"); if (TitleAttrib != null) { _ProductName = TitleAttrib.ConstructorArguments[0].Value.ToString(); } else { _ProductName = "Default Application Name"; } } return _ProductName; } } public Task ShowError(Exception error, string title, string buttonText, Action afterHideCallback) { return ShowError(error.Message, title, buttonText, afterHideCallback); } public Task ShowMessage(string message, string title) { return Task.Run(() => MessengerSend(message, title, MessageBoxButtonVM.OK, MessageBoxImageVM.Error)); } public Task ShowError(string message, string title, string buttonText, Action afterHideCallback) { return Task.Run(() => { MessengerSend(message, title, MessageBoxButtonVM.OK, MessageBoxImageVM.Error); afterHideCallback?.Invoke(); }); } public Task ShowMessage(string message, string title, string buttonText, Action afterHideCallback) { return Task.Run(() => { MessengerSend(message, title); afterHideCallback?.Invoke(); }); } public Task<bool> ShowMessage(string message, string title, string buttonConfirmText, string buttonCancelText, Action<bool> afterHideCallback) { if ((buttonConfirmText == "OK" && buttonCancelText == "Cancel") || (buttonConfirmText == "Yes" && buttonCancelText == "No")) { return Task.Run<bool>(() => { MessageBoxButtonVM btn; if (buttonConfirmText == "OK") btn = MessageBoxButtonVM.OKCancel; else btn = MessageBoxButtonVM.YesNo; bool Response = false; Messenger.Send(new MessageBoxNotificationWithAction<bool>( new MessageBoxArgs() { Buttons = btn, Icon = MessageBoxImageVM.Question, Title = (string.IsNullOrEmpty(title) ? _ProductName : title), Message = message }, (result) => Response = result )); afterHideCallback?.Invoke(Response); return Response; }); } else throw new ArgumentException($"{nameof(buttonConfirmText)} and {nameof(buttonCancelText)} must either be OK/Cancel or Yes/No."); } /// <summary> /// For debugging purpose only /// </summary> /// <param name="message"></param> /// <param name="title"></param> /// <returns></returns> public Task ShowMessageBox(string message, string title) => ShowMessage(message, title); private void MessengerSend(string msg, string title = "", MessageBoxButtonVM btn = MessageBoxButtonVM.OK, MessageBoxImageVM icon = MessageBoxImageVM.Information) { Messenger.Send(new MessageBoxArgs() { Buttons = MessageBoxButtonVM.OK, Icon = MessageBoxImageVM.Information, Title = (string.IsNullOrEmpty(title) ? _ProductName : title), Message = msg }); } } } 

这是表示层(进入View项目)

 using System.Windows; using VM; namespace View { class DialogPresenter { private Window _Parent; public DialogPresenter() { //For simple information boxes GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxArgs>(this, (arg) => ShowDialog(arg)); //For Yes/No or OK/Cancel dialog boxes. GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxNotificationWithAction<bool>>(this, (arg) => arg.Execute(ShowDialog(arg.Notification))); //For notifications that require a string response (such as Manual Timeslot Description) GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxNotificationWithAction<string>>(this, (arg) => arg.Execute(ShowStringInputDialog(arg.Notification.Title, arg.Notification.Message))); } private bool ShowDialog(MessageBoxArgs arg) { MessageBoxButton btn = MessageBoxButton.OK; MessageBoxImage ico = MessageBoxImage.None; switch (arg.Buttons) { case MessageBoxButtonVM.OK: btn = MessageBoxButton.OK; break; case MessageBoxButtonVM.OKCancel: btn = MessageBoxButton.OKCancel; break; case MessageBoxButtonVM.YesNo: btn = MessageBoxButton.YesNo; break; } switch (arg.Icon) { case MessageBoxImageVM.Error: ico = MessageBoxImage.Error; break; case MessageBoxImageVM.Information: ico = MessageBoxImage.Information; break; case MessageBoxImageVM.None: ico = MessageBoxImage.None; break; case MessageBoxImageVM.Question: ico = MessageBoxImage.Question; break; } bool Result = false; _Parent.Dispatcher.Invoke(() => { var Res = MessageBox.Show(arg.Message, arg.Title, btn, ico); Result = (Res == MessageBoxResult.OK || Res == MessageBoxResult.Yes); }); return Result; } private string ShowStringInputDialog(string title, string description, string value = "", int maxLength = 100) { string Result = null; _Parent.Dispatcher.Invoke(() => { //InputBox is a WPF Window I created for taking simple //string values from the user. This also shows that you can //any custom dialog using this approach. InputBox input = new InputBox(); input.Title = title; input.Owner = _Parent; if (input.ShowDialog(description, value, maxLength).Value) Result=input.Value; else Result=null; }); return Result; } //Call this somewhere at application startup so that the dialog boxes //appear as child windows. public void SetParentWindow(Window parent) { _Parent = parent; } } } 

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

根据事件/场景的不同,它也可能有一个订阅查看模型事件的事件触发器,以及一个或多个要响应的操作。

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

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

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

我喜欢这种服务方式 – 它不那么灵活,所以用户不太可能破坏某些东西:)它也与我的应用程序的WinForms部分一致(MessageBox.Show)但是如果你打算展示很多不同的对话框,那么InteractionRequest是一个更好的方式去。

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

我知道这是一个古老的问题,但是当我做这个search的时候,我发现了很多相关的问题,但是我没有find明确的答案。 所以我做我自己的对话框/ messagebox / popin的实现,我分享它!
我认为这是“MVVMcertificate”,我尽量使它简单适当,但我是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 */ }); 

它显示了这样的事情:

2

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

我目前的解决scheme如下所示:

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

当视图模型决定需要用户input时,它会提供SelectionTaskModel一个实例,并为用户提供可能的select。 基础设施负责提供相应的视图,并在适当的时候根据用户的select调用Choose()函数。

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

我在我的博客中展示了这一点:

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

你可以这样做的一个方法是

在您的主视图模型(您打开模式的位置)中:

 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. } } 

而在你的模态窗口视图/ 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> 

视图模型:

 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:如何closures一个窗口