仅在视图(不是窗口)上的WPF MVVM模态覆盖对话框

我对MVVM架构设计很陌生……

我最近在努力寻找一个已经为此目的编写的合适控件,但没有运气,所以我重新使用了来自另一个类似控件的部分XAML并自行制作。

我想要实现的是:

有一个可重用的View(usercontrol)+ viewmodel(要绑定到),以便能够在其他视图中使用作为模式覆盖,显示禁用视图其余部分的对话框,并在其上显示一个对话框。

在此处输入图像描述

我是如何实现它的:

  • 创建一个视图模型,它接受字符串(消息)和动作+字符串集合(按钮)
  • viewmodel创建一组调用这些操作的ICommands
  • 对话框视图绑定到其视图模型,该视图模型将作为另一个视图模型(父级)的属性公开
  • 对话框视图被放入父级的xaml中,如下所示:

pseudoXAML:

        

因此,modal dialog从Customer视图模型的DialogModel属性获取datacontext,并绑定命令和消息。 它也会绑定到对话框显示(绑定到IsShown)时需要禁用的其他元素(此处为’content’)。 当您单击对话框中的某个按钮时,将调用关联的命令,该命令只调用在viewmodel的构造函数中传递的关联操作。

这样我就可以从Customer视图模型中调用对话框视图模型上的对话框的Show()和Hide(),并根据需要更改对话框视图模型。

它会一次只给我一个对话但是没问题。 我还认为对话框视图模型将保持单一性,因为unit testing将涵盖在构造函数中使用Actions创建之后应该创建的命令的调用。 对话框视图会有几行代码隐藏,但非常小而且非常愚蠢(setter getters,几乎没有代码)。

我关心的是:

这个可以吗? 我有什么问题可以进入吗? 这会破坏一些MVVM原则吗?

非常感谢!

编辑:我发布了我的完整解决方案,以便您可以更好地了解。 欢迎任何建筑评论。 如果您看到一些可以更正的语法,则post会被标记为社区维基。

好吧不完全是我的问题的答案,但这里是这个对话框的结果,完成代码,所以你可以使用它,如果你愿意 – 免费在言论自由和啤酒:

MVVM对话框模式仅在包含视图内

另一个视图中的XAML用法(此处为CustomerView):

         

从父ViewModel(此处为CustomerViewModel)触发:

  public ModalDialogViewModel Dialog // dialog view binds to this { get { return _dialog; } set { _dialog = value; base.OnPropertyChanged("Dialog"); } } public void AskSave() { Action OkCallback = () => { if (Dialog != null) Dialog.Hide(); Save(); }; if (Email.Length < 10) { Dialog = new ModalDialogViewModel("This email seems a bit too short, are you sure you want to continue saving?", ModalDialogViewModel.DialogButtons.Ok, ModalDialogViewModel.CreateCommands(new Action[] { OkCallback })); Dialog.Show(); return; } if (LastName.Length < 2) { Dialog = new ModalDialogViewModel("The Lastname seems short. Are you sure that you want to save this Customer?", ModalDialogViewModel.CreateButtons(ModalDialogViewModel.DialogMode.TwoButton, new string[] {"Of Course!", "NoWay!"}, OkCallback, () => Dialog.Hide())); Dialog.Show(); return; } Save(); // if we got here we can save directly } 

这是代码:

ModalDialogView XAML:

                            

ModalDialogView代码背后:

 using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace DemoApp.View { ///  /// Interaction logic for ModalDialog.xaml ///  public partial class ModalDialog : UserControl { public ModalDialog() { InitializeComponent(); Visibility = Visibility.Hidden; } private bool _parentWasEnabled = true; public bool IsShown { get { return (bool)GetValue(IsShownProperty); } set { SetValue(IsShownProperty, value); } } // Using a DependencyProperty as the backing store for IsShown. This enables animation, styling, binding, etc... public static readonly DependencyProperty IsShownProperty = DependencyProperty.Register("IsShown", typeof(bool), typeof(ModalDialog), new UIPropertyMetadata(false, IsShownChangedCallback)); public static void IsShownChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { if ((bool)e.NewValue == true) { ModalDialog dlg = (ModalDialog)d; dlg.Show(); } else { ModalDialog dlg = (ModalDialog)d; dlg.Hide(); } } #region OverlayOn public UIElement OverlayOn { get { return (UIElement)GetValue(OverlayOnProperty); } set { SetValue(OverlayOnProperty, value); } } // Using a DependencyProperty as the backing store for Parent. This enables animation, styling, binding, etc... public static readonly DependencyProperty OverlayOnProperty = DependencyProperty.Register("OverlayOn", typeof(UIElement), typeof(ModalDialog), new UIPropertyMetadata(null)); #endregion public void Show() { // Force recalculate binding since Show can be called before binding are calculated BindingExpression expressionOverlayParent = this.GetBindingExpression(OverlayOnProperty); if (expressionOverlayParent != null) { expressionOverlayParent.UpdateTarget(); } if (OverlayOn == null) { throw new InvalidOperationException("Required properties are not bound to the model."); } Visibility = System.Windows.Visibility.Visible; _parentWasEnabled = OverlayOn.IsEnabled; OverlayOn.IsEnabled = false; } private void Hide() { Visibility = Visibility.Hidden; OverlayOn.IsEnabled = _parentWasEnabled; } } } 

ModalDialogViewModel:

 using System; using System.Windows.Input; using System.Collections.ObjectModel; using System.Collections.Generic; using System.Windows; using System.Linq; namespace DemoApp.ViewModel { ///  /// Represents an actionable item displayed by a View (DialogView). ///  public class ModalDialogViewModel : ViewModelBase { #region Nested types ///  /// Nested enum symbolizing the types of default buttons used in the dialog -> you can localize those with Localize(DialogMode, string[]) ///  public enum DialogMode { ///  /// Single button in the View (default: OK) ///  OneButton = 1, ///  /// Two buttons in the View (default: YesNo) ///  TwoButton, ///  /// Three buttons in the View (default: AbortRetryIgnore) ///  TreeButton, ///  /// Four buttons in the View (no default translations, use Translate) ///  FourButton, ///  /// Five buttons in the View (no default translations, use Translate) ///  FiveButton } ///  /// Provides some default button combinations ///  public enum DialogButtons { ///  /// As System.Window.Forms.MessageBoxButtons Enumeration Ok ///  Ok, ///  /// As System.Window.Forms.MessageBoxButtons Enumeration OkCancel ///  OkCancel, ///  /// As System.Window.Forms.MessageBoxButtons Enumeration YesNo ///  YesNo, ///  /// As System.Window.Forms.MessageBoxButtons Enumeration YesNoCancel ///  YesNoCancel, ///  /// As System.Window.Forms.MessageBoxButtons Enumeration AbortRetryIgnore ///  AbortRetryIgnore, ///  /// As System.Window.Forms.MessageBoxButtons Enumeration RetryCancel ///  RetryCancel } #endregion #region Members private static Dictionary _translations = null; private bool _dialogShown; private ReadOnlyCollection _commands; private string _dialogMessage; private string _dialogHeader; #endregion #region Class static methods and constructor ///  /// Creates a dictionary symbolizing buttons for given dialog mode and buttons names with actions to berform on each ///  /// Mode that tells how many buttons are in the dialog /// Names of buttons in sequential order /// Callbacks for given buttons ///  public static Dictionary CreateButtons(DialogMode mode, string[] names, params Action[] callbacks) { int modeNumButtons = (int)mode; if (names.Length != modeNumButtons) throw new ArgumentException("The selected mode needs a different number of button names", "names"); if (callbacks.Length != modeNumButtons) throw new ArgumentException("The selected mode needs a different number of callbacks", "callbacks"); Dictionary buttons = new Dictionary(); for (int i = 0; i < names.Length; i++) { buttons.Add(names[i], callbacks[i]); } return buttons; } ///  /// Static contructor for all DialogViewModels, runs once ///  static ModalDialogViewModel() { InitTranslations(); } ///  /// Fills the default translations for all modes that we support (use only from static constructor (not thread safe per se)) ///  private static void InitTranslations() { _translations = new Dictionary(); foreach (DialogMode mode in Enum.GetValues(typeof(DialogMode))) { _translations.Add(mode, GetDefaultTranslations(mode)); } } ///  /// Creates Commands for given enumeration of Actions ///  /// Actions to create commands from /// Array of commands for given actions public static ICommand[] CreateCommands(IEnumerable actions) { List commands = new List(); Action[] actionArray = actions.ToArray(); foreach (var action in actionArray) { //RelayExecuteWrapper rxw = new RelayExecuteWrapper(action); Action act = action; commands.Add(new RelayCommand(x => act())); } return commands.ToArray(); } ///  /// Creates string for some predefined buttons (English) ///  /// DialogButtons enumeration value /// String array for desired buttons public static string[] GetButtonDefaultStrings(DialogButtons buttons) { switch (buttons) { case DialogButtons.Ok: return new string[] { "Ok" }; case DialogButtons.OkCancel: return new string[] { "Ok", "Cancel" }; case DialogButtons.YesNo: return new string[] { "Yes", "No" }; case DialogButtons.YesNoCancel: return new string[] { "Yes", "No", "Cancel" }; case DialogButtons.RetryCancel: return new string[] { "Retry", "Cancel" }; case DialogButtons.AbortRetryIgnore: return new string[] { "Abort", "Retry", "Ignore" }; default: throw new InvalidOperationException("There are no default string translations for this button configuration."); } } private static string[] GetDefaultTranslations(DialogMode mode) { string[] translated = null; switch (mode) { case DialogMode.OneButton: translated = GetButtonDefaultStrings(DialogButtons.Ok); break; case DialogMode.TwoButton: translated = GetButtonDefaultStrings(DialogButtons.YesNo); break; case DialogMode.TreeButton: translated = GetButtonDefaultStrings(DialogButtons.YesNoCancel); break; default: translated = null; // you should use Translate() for this combination (ie. there is no default for four or more buttons) break; } return translated; } ///  /// Translates all the Dialogs with specified mode ///  /// Dialog mode/type /// Array of translations matching the buttons in the mode public static void Translate(DialogMode mode, string[] translations) { lock (_translations) { if (translations.Length != (int)mode) throw new ArgumentException("Wrong number of translations for selected mode"); if (_translations.ContainsKey(mode)) { _translations.Remove(mode); } _translations.Add(mode, translations); } } #endregion #region Constructors and initialization public ModalDialogViewModel(string message, DialogMode mode, params ICommand[] commands) { Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, _translations[mode], commands); } public ModalDialogViewModel(string message, DialogMode mode, params Action[] callbacks) { Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, _translations[mode], CreateCommands(callbacks)); } public ModalDialogViewModel(string message, Dictionary buttons) { Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, buttons.Keys.ToArray(), CreateCommands(buttons.Values.ToArray())); } public ModalDialogViewModel(string message, string header, Dictionary buttons) { if (buttons == null) throw new ArgumentNullException("buttons"); ICommand[] commands = CreateCommands(buttons.Values.ToArray()); Init(message, header, buttons.Keys.ToArray(), commands); } public ModalDialogViewModel(string message, DialogButtons buttons, params ICommand[] commands) { Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, ModalDialogViewModel.GetButtonDefaultStrings(buttons), commands); } public ModalDialogViewModel(string message, string header, DialogButtons buttons, params ICommand[] commands) { Init(message, header, ModalDialogViewModel.GetButtonDefaultStrings(buttons), commands); } public ModalDialogViewModel(string message, string header, string[] buttons, params ICommand[] commands) { Init(message, header, buttons, commands); } private void Init(string message, string header, string[] buttons, ICommand[] commands) { if (message == null) throw new ArgumentNullException("message"); if (buttons.Length != commands.Length) throw new ArgumentException("Same number of buttons and commands expected"); base.DisplayName = "ModalDialog"; this.DialogMessage = message; this.DialogHeader = header; List commandModels = new List(); // create commands viewmodel for buttons in the view for (int i = 0; i < buttons.Length; i++) { commandModels.Add(new CommandViewModel(buttons[i], commands[i])); } this.Commands = new ReadOnlyCollection(commandModels); } #endregion #region Properties ///  /// Checks if the dialog is visible, use Show() Hide() methods to set this ///  public bool DialogShown { get { return _dialogShown; } private set { _dialogShown = value; base.OnPropertyChanged("DialogShown"); } } ///  /// The message shown in the dialog ///  public string DialogMessage { get { return _dialogMessage; } private set { _dialogMessage = value; base.OnPropertyChanged("DialogMessage"); } } ///  /// The header (title) of the dialog ///  public string DialogHeader { get { return _dialogHeader; } private set { _dialogHeader = value; base.OnPropertyChanged("DialogHeader"); } } ///  /// Commands this dialog calls (the models that it binds to) ///  public ReadOnlyCollection Commands { get { return _commands; } private set { _commands = value; base.OnPropertyChanged("Commands"); } } #endregion #region Methods public void Show() { this.DialogShown = true; } public void Hide() { this._dialogMessage = String.Empty; this.DialogShown = false; } #endregion } } 

ViewModelBase具有:

public virtual string DisplayName { get; protected set; }

并实现INotifyPropertyChanged

放入资源字典的一些资源:

                  

我在GitHub页面上有一个自定义的开源FrameworkElement ,允许您在主要内容上显示模态内容。

控件可以像这样使用:

          

特征:

  • 显示任意内容。
  • 在显示模态内容时不禁用主要内容。
  • 在显示模态内容时禁用对主要内容的鼠标和键盘访问。
  • 仅对其所涵盖的内容进行模态处理,而不是整个应用程序。
  • 可以通过绑定到IsModal属性以MVVM友好的方式使用。

我会将此作为一个服务注入到您的ViewModel中,沿着下面的示例代码行。 在某种程度上你想要做的事实上是消息框行为,我会让我的服务实现使用MessageBox!

我在这里使用KISS来呈现这个概念。 没有代码,完全可以unit testing,如图所示。

顺便说一下,即使它没有涵盖所有内容,你正在努力工作的Josh Smith的例子对我也非常有帮助

HTH,
浆果

 ///  /// Simple interface for visually confirming a question to the user ///  public interface IConfirmer { bool Confirm(string message, string caption); } public class WPFMessageBoxConfirmer : IConfirmer { #region Implementation of IConfirmer public bool Confirm(string message, string caption) { return MessageBox.Show(message, caption, MessageBoxButton.YesNo) == MessageBoxResult.Yes; } #endregion } // SomeViewModel uses an IConfirmer public class SomeViewModel { public ShellViewModel(ISomeRepository repository, IConfirmer confirmer) { if (confirmer == null) throw new ArgumentNullException("confirmer"); _confirmer = confirmer; ... } ... private void _delete() { var someVm = _masterVm.SelectedItem; Check.RequireNotNull(someVm); if (detailVm.Model.IsPersistent()) { var msg = string.Format(GlobalCommandStrings.ConfirmDeletion, someVm.DisplayName); if(_confirmer.Confirm(msg, GlobalCommandStrings.ConfirmDeletionCaption)) { _doDelete(someVm); } } else { _doDelete(someVm); } } ... } // usage in the Production code var vm = new SomeViewModel(new WPFMessageBoxConfirmer()); // usage in a unit test [Test] public void DeleteCommand_OnExecute_IfUserConfirmsDeletion_RemovesSelectedItemFrom_Workspaces() { var confirmerMock = MockRepository.GenerateStub(); confirmerMock.Stub(x => x.Confirm(Arg.Is.Anything, Arg.Is.Anything)).Return(true); var vm = new ShellViewModel(_repository, _crudConverter, _masterVm, confirmerMock, _validator); vm.EditCommand.Execute(null); Assert.That(vm.Workspaces, Has.Member(_masterVm.SelectedItem)); Assert.That(vm.Workspaces, Is.Not.Empty); vm.DeleteCommand.Execute(null); Assert.That(vm.Workspaces, Has.No.Member(_masterVm.SelectedItem)); Assert.That(vm.Workspaces, Is.Empty); }