WPF MVVM模态叠加对话框仅在视图(而非窗口)

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

最近我在努力寻找一个已经为此目的而编写的合适的控件,但没有运气,所以我从另一个类似的控件中重用了部分XAML,并取得了自己的成果。

我想要达到的是:

有一个可重复使用的View(usercontrol)+ viewmodel(绑定到)可以在其他视图中用作模式覆盖图,显示禁用其余视图的对话框,并显示对话框。

在这里输入图像描述

我想如何实现它:

  • 创建一个视图模型,它接受字符串(消息)和动作+字符串集合(按钮)
  • 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 viewmodel的DialogModel属性获取datacontext,并绑定命令和消息。 它也会绑定到其他元素(这里是'content'),当对话框显示时(绑定到IsShown)需要被禁用。 在对话框中单击某个按钮时,将调用相关的命令,该命令仅调用在视图模型的构造函数中传递的关联操作。

    通过这种方式,我可以在Customer viewmodel中调用对话框viewmodel中的对话框的Show()和Hide(),并根据需要更改对话框视图模型。

    它一次只能给我一个对话框,但这很好。 我也认为对话框viewmodel将保持单元测试,因为unittests将覆盖调用在构造函数中使用Actions创建的应该创建的命令。 对话框视图将会有几行代码隐藏,但非常少且非常愚蠢(setter getter,几乎没有代码)。

    我担心的是:

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

    非常感谢!

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


    这个问题不完全是我的问题的答案,但这里是做这个对话框的结果,它包含代码,所以如果你愿意,你可以使用它 - 免费的言论和啤酒:

    仅在包含视图中的MVVM对话框模式

    另一个视图中的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>
    

    我在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);
    }
    
    链接地址: http://www.djcxy.com/p/56125.html

    上一篇: WPF MVVM Modal Overlay Dialog only over a View (not Window)

    下一篇: Handling the window closing event with WPF / MVVM Light Toolkit