c# - WPF MVVM 模态叠加对话框仅在 View (而非窗口)上

标签 c# wpf mvvm wpf-controls

我对 MVVM 架构设计非常陌生...

我最近一直在努力寻找已经为此类目的编写的合适控件,但运气不佳,因此我重用了另一个类似控件中的部分 XAML,并制作了自己的控件。

我想实现的是:

有一个可重复使用的 View (用户控件)+ View 模型(绑定(bind)到),以便能够在其他 View 中用作模态覆盖,显示一个对话框,该对话框禁用 View 的其余部分,并在其上方显示一个对话框。

enter image description here

我想如何实现它:

  • 创建一个接受字符串(消息)和操作+字符串集合(按钮)的 View 模型
  • viewmodel 创建调用这些操作的 ICommand 集合
  • 对话框 View 绑定(bind)到其 View 模型,该 View 模型将作为另一个 View 模型(父)的属性公开
  • 对话框 View 像这样放入父级的 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>

所以这里的模态对话框从 Customer View 模型的 DialogModel 属性获取数据上下文,并绑定(bind)命令和消息。它还将绑定(bind)到一些其他元素(此处为“内容”),当对话框显示时需要将其禁用(绑定(bind)到 IsShown)。当您单击对话框中的某个按钮时,将调用关联的命令,该命令仅调用在 View 模型的构造函数中传递的关联操作。

这样我就可以从客户 View 模型内部调用对话框 View 模型上对话框的 Show() 和 Hide(),并根据需要更改对话框 View 模型。

它一次只会给我一个对话框,但这很好。 我还认为对话框 View 模型将保持单元测试,因为单元测试将涵盖在构造函数中使用 Actions 创建后应该创建的命令的调用。对话框 View 会有几行代码隐藏,但非常少而且非常愚蠢(setters getters,几乎没有代码)。

我关心的是:

这样可以吗? 我会遇到什么问题吗? 这是否违反了一些 MVVM 原则?

非常感谢!

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

最佳答案

这不是我问题的确切答案,但这是执行此对话的结果,并附有代码,因此您可以根据需要使用它 - 就像言论自由和啤酒一样自由:

MVVM dialog modal only inside the containing view

XAML 在另一个 View 中的用法(此处为 CustomerView):

<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,0,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, 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
{

    /// <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, DialogMode mode, params Action[] callbacks)
        {
            Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, _translations[mode], CreateCommands(callbacks));
        }

        public ModalDialogViewModel(string message, Dictionary<string, Action> buttons)
        {
            Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, buttons.Keys.ToArray(), CreateCommands(buttons.Values.ToArray()));
        }

        public ModalDialogViewModel(string message, string header, Dictionary<string, Action> buttons)
        {
            if (buttons == null)
                throw new ArgumentNullException("buttons");

            ICommand[] commands = CreateCommands(buttons.Values.ToArray<Action>());

            Init(message, header, buttons.Keys.ToArray<string>(), 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<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;保护集; }

并实现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>

关于c# - WPF MVVM 模态叠加对话框仅在 View (而非窗口)上,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6351612/

相关文章:

c# - 为什么使用不存在的字体也没有异常?

c# - 如何根据键值获取在文本文件中输入的记录

c# - 在 Visual Studio 2013 中访问抽象语法树

c# - 如何在Moq中更改输入参数?

c# - 在 WPF 中作为资源播放声音

mvvm - TextBox.Text 单向绑定(bind)不响应 ViewModel 更新背后的代码

c# - x :Name in XAML WPF - Visual Studio Community 2017 BUG

c# - 在浏览器中托管 Word - AutomationElement IsWindowPatternAvailable - 如何设置?

java - MVVM - 如何将上下文传递给存储库类?

c# - 关于接口(interface)实现和可扩展性的架构问题