web-dev-qa-db-fra.com

Boîte de dialogue de superposition modale WPF MVVM uniquement sur une vue (pas une fenêtre)

Je suis à peu près nouveau dans la conception de l'architecture MVVM ...

J'ai eu du mal ces derniers temps à trouver un contrôle approprié déjà écrit à cette fin, mais je n'ai pas eu de chance, j'ai donc réutilisé des parties de XAML à partir d'un autre contrôle similaire et j'ai créé le mien.

Ce que je veux réaliser, c'est:

Avoir une vue réutilisable (usercontrol) + viewmodel (pour se lier à) pour pouvoir utiliser à l'intérieur d'autres vues comme une superposition modale montrant une boîte de dialogue qui désactive le reste de la vue et affiche une boîte de dialogue par-dessus.

enter image description here

Comment je voulais l'implémenter:

  • créer un viewmodel qui prend la chaîne (message) et la collection action + chaîne (boutons)
  • viewmodel crée une collection de ICommands qui appellent ces actions
  • la vue de dialogue se lie à son viewmodel qui sera exposé comme propriété d'un autre viewmodel (parent)
  • la vue de dialogue est placée dans le xaml du parent comme ceci:

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>

Donc, ici, la boîte de dialogue modale obtient le datacontext de la propriété DialogModel du viewmodel Customer et lie les commandes et le message. Il serait également lié à un autre élément (ici "contenu") qui doit être désactivé lorsque la boîte de dialogue s'affiche (liaison à IsShown). Lorsque vous cliquez sur un bouton de la boîte de dialogue, la commande associée est appelée, qui appelle simplement l'action associée qui a été transmise au constructeur du modèle de vue.

De cette façon, je serais en mesure d'appeler Show () et Hide () de la boîte de dialogue sur le modèle de vue de dialogue depuis l'intérieur du modèle de vue Client et de modifier le modèle de vue de dialogue selon les besoins.

Cela ne me donnerait qu'un seul dialogue à la fois, mais ça va. Je pense également que le modèle d'affichage de la boîte de dialogue resterait inaltérable, car les unités de contrôle couvriraient l'appel des commandes qui devraient être créées après sa création avec Actions dans le constructeur. Il y aurait quelques lignes de code pour la vue de dialogue, mais très peu et assez stupides (setters getters, avec presque pas de code).

Ce qui me préoccupe c'est:

Est-ce correct? Y a-t-il des problèmes que je pourrais rencontrer? Est-ce que cela brise certains principes MVVM?

Merci beaucoup!

EDIT: J'ai posté ma solution complète pour que vous puissiez mieux voir. Tous les commentaires architecturaux sont les bienvenus. Si vous voyez une syntaxe qui peut être corrigée, le message est signalé comme wiki communautaire.

28
Marino Šimić

Eh bien, ce n'est pas exactement une réponse à ma question, mais voici le résultat de cette boîte de dialogue, avec du code afin que vous puissiez l'utiliser si vous le souhaitez - gratuit comme dans la liberté d'expression et la bière:

MVVM dialog modal only inside the containing view

tilisation XAML dans une autre vue (ici 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>

Déclenchement à partir du ViewModel parent (ici 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
    }

Voici le 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>

code ModalDialogView derrière:

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 a:

public virtual string DisplayName { get; protected set; }

et implémente INotifyPropertyChanged

Quelques ressources à mettre dans le dictionnaire des ressources:

<!--
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>
20
Marino Šimić

J'ai un open source personnalisé FrameworkElement sur ma page GitHub qui vous permet d'afficher du contenu modal sur le contenu principal.

Le contrôle peut être utilisé comme ceci:

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

Fonctionnalités:

  • Affiche du contenu arbitraire.
  • Ne désactive pas le contenu principal lorsque le contenu modal est affiché.
  • Désactive l'accès de la souris et du clavier au contenu principal pendant que le contenu modal est affiché.
  • Est uniquement modal par rapport au contenu qu'il couvre, pas à l'ensemble de l'application.
  • peut être utilisé de manière conviviale MVVM en se liant à la propriété IsModal.
9
Benjamin Gale

Je considérerais cela comme un service qui est injecté dans votre ViewModel, le long des lignes de l'exemple de code ci-dessous. Dans la mesure où ce que vous voulez faire est en fait un comportement de boîte de message, je voudrais que mon implémentation de service utilise un MessageBox!

J'utilise KISS ici pour présenter le concept. Aucun code derrière, et complètement testable à l'unité comme indiqué.

En passant, cet exemple de Josh Smith sur lequel vous travaillez m'a été incroyablement utile, même s'il ne couvre pas tout

HTH,
Baie

/// <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);
}
1
Berryl