Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
276 views
in Technique[技术] by (71.8m points)

c# - WPF MVVM Modal Overlay Dialog only over a View (not Window)

I'm pretty much new to the MVVM architecture design...

I was struggling lately to find a suitable control already written for such a purpose but had no luck, so I reused parts of XAML from another similar control and got make my own.

What I want to achieve is:

Have a reusable View (usercontrol) + viewmodel (to bind to) to be able to use inside other views as a modal overlay showing a dialog that disables the rest of the view, and shows a dialog over the it.

enter image description here

How I wanted to implement it:

  • create a viewmodel that takes string(message) and action+string collection(buttons)
  • viewmodel creates a collection of ICommands that call those actions
  • dialog view binds to the its viewmodel that will be exposed as property of another viewmodel (parent)
  • dialog view is put into the xaml of the parent like this:

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>

So here the modal dialog gets the datacontext from the DialogModel property of the Customer viewmodel, and binds commands and message. It would be also bound to some other element (here 'content') that needs to be disabled when the dialog shows (binding to IsShown). When you click some button in the dialog the associated command is called that simply calls the associated action that was passed in the constructor of the viewmodel.

This way I would be able to call Show() and Hide() of the dialog on the dialog viewmodel from inside the Customer viewmodel and alter the dialog viewmodel as needed.

It would give me only one dialog at a time but that is fine. I also think that the dialog viewmodel would remain unittestable, since the unittests would cover the calling of the commands that ought to be created after it being created with Actions in the constructor. There would be a few lines of codebehind for the dialog view, but very little and pretty dumb (setters getters, with almost no code).

What concerns me is:

Is this ok? Are there any problems I could get into? Does this break some MVVM principles?

Thanks a lot!

EDIT: I posted my complete solution so you can have a better look. Any architectural comments welcome. If you see some syntax that can be corrected the post is flagged as community wiki.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Well not exactly an answer to my question, but here is the result of doing this dialog, complete with code so you can use it if you wish - free as in free speech and beer:

MVVM dialog modal only inside the containing view

XAML Usage in another view (here 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>

Triggering from parent ViewModel (here 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
    }

Here is the code:

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 code behind:

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.Len

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...