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

时间:2011-06-15 00:07:02

标签: c# wpf mvvm wpf-controls

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

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

我想要实现的目标是:

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

enter image description here

我想如何实现它:

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

pseudoXAML:

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

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

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

我关心的是:

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

非常感谢!

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

3 个答案:

答案 0 :(得分:20)

这不是我的问题的答案,但是这里是完成代码的结果,如果您愿意,可以使用它 - 免费提供免费演讲和啤酒:

MVVM dialog modal only inside the containing view

另一个视图中的XAML用法(此处为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; 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>

答案 1 :(得分:9)

我的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友好的方式使用。

答案 2 :(得分:1)

我会将此作为一种服务注入到您的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);
}