如何解决WPF MVVM模态叠加对话框仅在视图而不是窗口上方
|| 我对MVVM架构设计非常陌生... 最近,我一直在努力寻找一个合适的控件,该控件已经为此目的而编写,但是没有运气,所以我重用了另一个类似控件中的XAML部分,并制作了自己的控件。 我要实现的是: 有一个可重用的View(用户控件)+ viewmodel(绑定到),以便能够在其他视图内部用作模式叠加层,该模式叠加层显示禁用其余视图的对话框,并在其上显示一个对话框。 我想如何实现它: 创建一个接受字符串(消息)和动作+字符串集合(按钮)的视图模型 viewmodel创建一个调用这些动作的ICommands的集合 对话框视图绑定到其视图模型,该视图模型将作为另一个视图模型(父视图)的属性公开 对话框视图被放入父级的xaml中,如下所示: 伪XAML: <usercontrol /customerview/ ...>
<grid>
<grid x:Name=\"content\">
<varIoUs form content />
</grid>
<ctrl:Dialog DataContext=\"{Binding DialogModel}\" Message=\"{Binding Message}\" Commands=\"{Binding Commands}\" IsShown=\"{Binding IsShown}\" BlockedUI=\"{Binding ElementName=content}\" />
</grid>
</usercontrol>
因此,模态对话框从“客户”视图模型的DialogModel属性获取数据上下文,并绑定命令和消息。它也将绑定到对话框显示时(绑定到IsShown)需要禁用的某些其他元素(此处为\'content \')。当您单击对话框中的某个按钮时,将调用关联的命令,该命令仅调用在视图模型的构造函数中传递的关联动作。
这样,我将能够从Customer视图模型内部在对话框视图模型上调用对话框的Show()和Hide(),并根据需要更改对话框视图模型。
一次只给我一个对话,但这很好。
我还认为,对话框视图模型将保持可测试性,因为单元测试将覆盖在构造函数中使用Actions创建后应创建的命令的调用。对话框视图后面会有几行代码,但是却很少而且很愚蠢(设置吸气剂,几乎没有代码)。
我担心的是:
这个可以吗?
我有什么问题可以解决吗?
这会破坏某些MVVM原理吗?
非常感谢!
编辑:我发布了完整的解决方案,以便您可以拥有更好的外观。欢迎任何建筑评论。如果您看到一些可以纠正的语法,则该帖子将标记为社区Wiki。
解决方法
好吧,这并不是我的问题的完全答案,但这是执行此对话框的结果,并附有代码,因此您可以根据需要使用它-免费,如言论自由和啤酒:
另一个视图(此处为CustomerView)中的XAML用法:
<UserControl
x:Class=\"DemoApp.View.CustomerView\"
xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"
xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"
xmlns:controls=\"clr-namespace:DemoApp.View\"
>
<Grid>
<Grid Margin=\"4\" x:Name=\"ModalDialogParent\">
<put all view content here/>
</Grid>
<controls:ModalDialog DataContext=\"{Binding Dialog}\" OverlayOn=\"{Binding ElementName=ModalDialogParent,Mode=OneWay}\" IsShown=\"{Binding Path=DialogShown}\"/>
</Grid>
</UserControl>
从父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:
<UserControl x:Class=\"DemoApp.View.ModalDialog\"
xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"
xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"
x:Name=\"root\">
<UserControl.Resources>
<ResourceDictionary Source=\"../MainWindowResources.xaml\" />
</UserControl.Resources>
<Grid>
<Border Background=\"#90000000\" Visibility=\"{Binding Visibility}\">
<Border BorderBrush=\"Black\" BorderThickness=\"1\" Background=\"AliceBlue\"
CornerRadius=\"10,10,0\" VerticalAlignment=\"Center\"
HorizontalAlignment=\"Center\">
<Border.BitmapEffect>
<DropShadowBitmapEffect Color=\"Black\" Opacity=\"0.5\" Direction=\"270\" ShadowDepth=\"0.7\" />
</Border.BitmapEffect>
<Grid Margin=\"10\">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition Height=\"Auto\" />
</Grid.RowDefinitions>
<TextBlock Style=\"{StaticResource ModalDialogHeader}\" Text=\"{Binding DialogHeader}\" Grid.Row=\"0\"/>
<TextBlock Text=\"{Binding DialogMessage}\" Grid.Row=\"1\" TextWrapping=\"Wrap\" Margin=\"5\" />
<StackPanel HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Bottom\" Grid.Row=\"2\">
<ContentControl HorizontalAlignment=\"Stretch\"
DataContext=\"{Binding Commands}\"
Content=\"{Binding}\"
ContentTemplate=\"{StaticResource ButtonCommandsTemplate}\"
/>
</StackPanel>
</Grid>
</Border>
</Border>
</Grid>
</UserControl>
ModalDialogView的代码背后:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DemoApp.View
{
/// <summary>
/// Interaction logic for ModalDialog.xaml
/// </summary>
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,etc...
public static readonly DependencyProperty OverlayOnProperty =
DependencyProperty.Register(\"OverlayOn\",typeof(UIElement),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
{
/// <summary>
/// Represents an actionable item displayed by a View (DialogView).
/// </summary>
public class ModalDialogViewModel : ViewModelBase
{
#region Nested types
/// <summary>
/// Nested enum symbolizing the types of default buttons used in the dialog -> you can localize those with Localize(DialogMode,string[])
/// </summary>
public enum DialogMode
{
/// <summary>
/// Single button in the View (default: OK)
/// </summary>
OneButton = 1,/// <summary>
/// Two buttons in the View (default: YesNo)
/// </summary>
TwoButton,/// <summary>
/// Three buttons in the View (default: AbortRetryIgnore)
/// </summary>
TreeButton,/// <summary>
/// Four buttons in the View (no default translations,use Translate)
/// </summary>
FourButton,/// <summary>
/// Five buttons in the View (no default translations,use Translate)
/// </summary>
FiveButton
}
/// <summary>
/// Provides some default button combinations
/// </summary>
public enum DialogButtons
{
/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration Ok
/// </summary>
Ok,/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration OkCancel
/// </summary>
OkCancel,/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration YesNo
/// </summary>
YesNo,/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration YesNoCancel
/// </summary>
YesNoCancel,/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration AbortRetryIgnore
/// </summary>
AbortRetryIgnore,/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration RetryCancel
/// </summary>
RetryCancel
}
#endregion
#region Members
private static Dictionary<DialogMode,string[]> _translations = null;
private bool _dialogShown;
private ReadOnlyCollection<CommandViewModel> _commands;
private string _dialogMessage;
private string _dialogHeader;
#endregion
#region Class static methods and constructor
/// <summary>
/// Creates a dictionary symbolizing buttons for given dialog mode and buttons names with actions to berform on each
/// </summary>
/// <param name=\"mode\">Mode that tells how many buttons are in the dialog</param>
/// <param name=\"names\">Names of buttons in sequential order</param>
/// <param name=\"callbacks\">Callbacks for given buttons</param>
/// <returns></returns>
public static Dictionary<string,Action> 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<string,Action> buttons = new Dictionary<string,Action>();
for (int i = 0; i < names.Length; i++)
{
buttons.Add(names[i],callbacks[i]);
}
return buttons;
}
/// <summary>
/// Static contructor for all DialogViewModels,runs once
/// </summary>
static ModalDialogViewModel()
{
InitTranslations();
}
/// <summary>
/// Fills the default translations for all modes that we support (use only from static constructor (not thread safe per se))
/// </summary>
private static void InitTranslations()
{
_translations = new Dictionary<DialogMode,string[]>();
foreach (DialogMode mode in Enum.GetValues(typeof(DialogMode)))
{
_translations.Add(mode,GetDefaultTranslations(mode));
}
}
/// <summary>
/// Creates Commands for given enumeration of Actions
/// </summary>
/// <param name=\"actions\">Actions to create commands from</param>
/// <returns>Array of commands for given actions</returns>
public static ICommand[] CreateCommands(IEnumerable<Action> actions)
{
List<ICommand> commands = new List<ICommand>();
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();
}
/// <summary>
/// Creates string for some predefined buttons (English)
/// </summary>
/// <param name=\"buttons\">DialogButtons enumeration value</param>
/// <returns>String array for desired buttons</returns>
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;
}
/// <summary>
/// Translates all the Dialogs with specified mode
/// </summary>
/// <param name=\"mode\">Dialog mode/type</param>
/// <param name=\"translations\">Array of translations matching the buttons in the mode</param>
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,params Action[] callbacks)
{
Init(message,CreateCommands(callbacks));
}
public ModalDialogViewModel(string message,Dictionary<string,Action> buttons)
{
Init(message,buttons.Keys.ToArray(),CreateCommands(buttons.Values.ToArray()));
}
public ModalDialogViewModel(string message,string header,Action> buttons)
{
if (buttons == null)
throw new ArgumentNullException(\"buttons\");
ICommand[] commands = CreateCommands(buttons.Values.ToArray<Action>());
Init(message,header,buttons.Keys.ToArray<string>(),DialogButtons buttons,ModalDialogViewModel.GetButtonDefaultStrings(buttons),string[] buttons,buttons,commands);
}
private void Init(string message,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<CommandViewModel> commandModels = new List<CommandViewModel>();
// 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<CommandViewModel>(commandModels);
}
#endregion
#region Properties
/// <summary>
/// Checks if the dialog is visible,use Show() Hide() methods to set this
/// </summary>
public bool DialogShown
{
get
{
return _dialogShown;
}
private set
{
_dialogShown = value;
base.OnPropertyChanged(\"DialogShown\");
}
}
/// <summary>
/// The message shown in the dialog
/// </summary>
public string DialogMessage
{
get
{
return _dialogMessage;
}
private set
{
_dialogMessage = value;
base.OnPropertyChanged(\"DialogMessage\");
}
}
/// <summary>
/// The header (title) of the dialog
/// </summary>
public string DialogHeader
{
get
{
return _dialogHeader;
}
private set
{
_dialogHeader = value;
base.OnPropertyChanged(\"DialogHeader\");
}
}
/// <summary>
/// Commands this dialog calls (the models that it binds to)
/// </summary>
public ReadOnlyCollection<CommandViewModel> 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
要放入资源字典中的一些资源:
<!--
This style gives look to the dialog head (used in the modal dialog)
-->
<Style x:Key=\"ModalDialogHeader\" TargetType=\"{x:Type TextBlock}\">
<Setter Property=\"Background\" Value=\"{StaticResource Brush_HeaderBackground}\" />
<Setter Property=\"Foreground\" Value=\"White\" />
<Setter Property=\"Padding\" Value=\"4\" />
<Setter Property=\"HorizontalAlignment\" Value=\"Stretch\" />
<Setter Property=\"Margin\" Value=\"5\" />
<Setter Property=\"TextWrapping\" Value=\"NoWrap\" />
</Style>
<!--
This template explains how to render the list of commands as buttons (used in the modal dialog)
-->
<DataTemplate x:Key=\"ButtonCommandsTemplate\">
<ItemsControl IsTabStop=\"False\" ItemsSource=\"{Binding}\" Margin=\"6,2\">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button MinWidth=\"75\" Command=\"{Binding Path=Command}\" Margin=\"4\" HorizontalAlignment=\"Right\">
<TextBlock Text=\"{Binding Path=DisplayName}\" Margin=\"2\"></TextBlock>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation=\"Horizontal\" HorizontalAlignment=\"Center\" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
, 我在GitHub页面上有一个自定义开源FrameworkElement
,可让您在主要内容上显示模式内容。
该控件可以像这样使用:
<c:ModalContentPresenter IsModal=\"{Binding DialogIsVisible}\">
<TabControl Margin=\"5\">
<Button Margin=\"55\"
Padding=\"10\"
Command=\"{Binding ShowModalContentCommand}\">
This is the primary Content
</Button>
</TabItem>
</TabControl>
<c:ModalContentPresenter.ModalContent>
<Button Margin=\"75\"
Padding=\"50\"
Command=\"{Binding HideModalContentCommand}\">
This is the modal content
</Button>
</c:ModalContentPresenter.ModalContent>
</c:ModalContentPresenter>
特征:
显示任意内容。
在显示模式内容时,不要禁用主要内容。
在显示模式内容时,禁用鼠标和键盘对主要内容的访问。
只是对所覆盖内容的模态,而不是整个应用程序的模态。
通过绑定到IsModal
属性,可以以MVVM友好的方式使用它。
, 我将其作为一种服务插入到您的ViewModel中,就像下面的示例代码一样。在某种程度上您实际上想做的是消息框行为,我希望我的服务实现使用MessageBox!
我在这里使用KISS来介绍这个概念。如图所示,没有任何代码,并且可以完全进行单元测试。
顺便说一句,您正在研究的乔希·史密斯(Josh Smith)示例对我也非常有用,即使它并不能涵盖所有内容
HTH,
浆果
/// <summary>
/// Simple interface for visually confirming a question to the user
/// </summary>
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<IConfirmer>();
confirmerMock.Stub(x => x.Confirm(Arg<string>.Is.Anything,Arg<string>.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);
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。