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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...