仅在视图(不是窗口)上的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会被标记为社区维基。
好吧不完全是我的问题的答案,但这里是这个对话框的结果,完成代码,所以你可以使用它,如果你愿意 – 免费在言论自由和啤酒:
另一个视图中的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); }