MVVM设计模式中的确认窗口

3

你好,我想在按钮点击时显示确认窗口。 我正在尝试使用MVVM设计模式进行开发,我已经实现了它,但我认为在viewModel中调用view并不是正确的做法。
我附上了代码,请浏览一下,这样做是否正确。

<Window x:Class="MessegeBox_Demo_2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Button Content="Ckick ME" HorizontalAlignment="Left" 
                    Command="{Binding GetMessegeboxCommand}"
                    Margin="200,131,0,0" VerticalAlignment="Top" Width="75"/>

        </Grid>
    </Window>

public class MainWindowViewModel : ViewModelBaseClass
{
   private ICommand _getMessegeboxCommand;
   public ICommand GetMessegeboxCommand
   {
      get
      {
          return _getMessegeboxCommand ?? (_getMessegeboxCommand = new MessegeBox_Demo_2.Command.realyCommand(() => ShowUsercontrol(), true));
      }
   }
   private void ShowUsercontrol()
   {
      MessegeBox_Demo_2.View.Window1 mbox = new View.Window1();
      mbox.ShowDialog();
   }
}

2
你可以实现一个对话服务来打开对话框,或者使用消息以MVVM方式实现关注点的清晰分离。 - nabulke
这里有几个关于如何在ViewModel事件的响应中显示新视图(例如对话框窗口)的问题。您已经完成的很好,只要它能正常工作。从纯粹主义者的角度来看,您可以考虑使用DependencyProperties为View提供一种机制,以便检测到ViewModel想要确认窗口,而无需ViewModel直接调用View。 - Evil Dog Pie
5个回答

10

最简单的方法是实现一个对话框服务并使用依赖注入将服务注入到视图模型中。依赖于接口是可以的,但不要依赖于具体实现。
以下是我使用的接口:

namespace DialogServiceInterfaceLibrary
{
    public enum MessageBoxButton  
    {
    // Summary:
    //     The message box displays an OK button.
    OK = 0,
    //
    // Summary:
    //     The message box displays OK and Cancel buttons.
    OKCancel = 1,
    //
    // Summary:
    //     The message box displays Yes, No, and Cancel buttons.
    YesNoCancel = 3,
    //
    // Summary:
    //     The message box displays Yes and No buttons.
    YesNo = 4,
    }

    public enum MessageBoxResult
    {
    // Summary:
    //     The message box returns no result.
    None = 0,
    //
    // Summary:
    //     The result value of the message box is OK.
    OK = 1,
    //
    // Summary:
    //     The result value of the message box is Cancel.
    Cancel = 2,
    //
    // Summary:
    //     The result value of the message box is Yes.
    Yes = 6,
    //
    // Summary:
    //     The result value of the message box is No.
    No = 7,
    }

    // Summary:
    //     Specifies the icon that is displayed by a message box.
    public enum MessageBoxIcon
    {
    // Summary:
    //     No icon is displayed.
    None = 0,
    //
    // Summary:
    //     The message box contains a symbol consisting of white X in a circle with
    //     a red background.
    Error = 16,
    //
    // Summary:
    //     The message box contains a symbol consisting of a white X in a circle with
    //     a red background.
    Hand = 16,
    //
    // Summary:
    //     The message box contains a symbol consisting of white X in a circle with
    //     a red background.
    Stop = 16,
    //
    // Summary:
    //     The message box contains a symbol consisting of a question mark in a circle.
    Question = 32,
    //
    // Summary:
    //     The message box contains a symbol consisting of an exclamation point in a
    //     triangle with a yellow background.
    Exclamation = 48,
    //
    // Summary:
    //     The message box contains a symbol consisting of an exclamation point in a
    //     triangle with a yellow background.
    Warning = 48,
    //
    // Summary:
    //     The message box contains a symbol consisting of a lowercase letter i in a
    //     circle.
    Information = 64,
    //
    // Summary:
    //     The message box contains a symbol consisting of a lowercase letter i in a
    //     circle.
    Asterisk = 64,
    }

    public interface IDialogService
    {
        bool OpenFileDialog(bool checkFileExists,string Filter, out string FileName);
        void OpenGenericDialog(object Context,IRegionManager RegionManager);
    MessageBoxResult ShowMessageBox(string message, string caption, MessageBoxButton buttons, MessageBoxIcon icon);
    }

而实现方式如下:
public class DialogService : IDialogService
{
    public bool OpenFileDialog(bool checkFileExists, string Filter, out string FileName) 
    {
        FileName = ""; 
        OpenFileDialog openFileDialog = new OpenFileDialog();
        openFileDialog.Multiselect = false;
        //openFileDialog.Filter = "All Image Files | *.jpg;*.png | All files | *.*";
        openFileDialog.Filter = Filter;
        openFileDialog.CheckFileExists = checkFileExists;
        bool result = ((bool)openFileDialog.ShowDialog());
        if (result)
        {
            FileName = openFileDialog.FileName;
        }
        return result;
    }


    public void OpenGenericDialog(object Context,IRegionManager RegionManager)
    {
        GenericDialogWindow dlg = new GenericDialogWindow(Context,RegionManager);
        dlg.Owner = System.Windows.Application.Current.MainWindow;
        dlg.Show();
    }

    public MessageBoxResult ShowMessageBox(string message, string caption, MessageBoxButton buttons, MessageBoxIcon icon)
    {
        return (DialogServiceInterfaceLibrary.MessageBoxResult)System.Windows.MessageBox.Show(message, caption, 
            (System.Windows.MessageBoxButton)buttons,
            (System.Windows.MessageBoxImage)icon);
    }
}

然后将IDialogservice注入到viewmodel中。接口和具体实现通常在不同的程序集中。

MainWindowViewModel(IDialogService dialogservice){
    _dialogservice = dialogservice;
}

private void ShowUsercontrol()
{
   _dialogservice.ShowMessageBox(... //you get what i mean ;-)
}

对话框服务可以打开标准的窗口,例如文件打开对话框。通用版本也可以使用,但是需要了解稍微复杂一些的prism。
Prism还允许使用交互请求在视图模型和视图之间通信。我更喜欢在prism中采用这种工作方式,但是如果您不了解prism,请忽略该备注。对于简单的确认窗口,这样一个简单的对话框服务非常理想。
通过将依赖项注入到视图模型的构造函数中,使您已经朝着使用反转控制容器的正确方向前进。并且可以更轻松地进行单元测试,因为您可以模拟注入的接口以检查它们是否被正确调用等。

谢谢您的帮助,我需要添加任何外部 DLL 吗? - Rahul Rathod
这取决于您的要求。如果您想严格执行,则接口和实现各有一个单独的程序集。您需要引用接口程序集。如果您不使用IOC容器,则还需要引用impl程序集。 - Philip Stuyck
请问您能否解释一下以下两点:1. IRegionManager RegionManager 2. 命名空间 GenericDialogWindow 存在于哪里? - Rahul Rathod
属于prism的部分,你不需要OpenGenericDialog,只需将其留空以适应你的需求。你只需要一个简单的确认对话框,使用ShowMessageBox方法即可。 - Philip Stuyck
谢谢,现在它可以工作了......起初我在IRegionManager上感到困惑....再次感谢你的帮助兄弟。 - Rahul Rathod
显示剩余2条评论

3
  • 在这种情况下,DialogService方法比消息机制更适合使用。它更直接,更容易调试,更容易编写和理解,您不需要任何第三方框架等。

  • TIP:依赖注入很好,但通常您需要相当多的服务,例如NavigationService、DialogService、NotificationService等等,如果您需要将它们注入到许多ViewModels中,则构造函数变得很大,很无聊,重复相同的注入等等。除了依赖注入之外,您还可以使用任何其他“可测试”的方法。

由于DialogService在所有的viewmodels中都是相同的,所以您并不一定需要将其注入到每个viewmodel中,而是可以使用某种AppContext类或服务定位器。

ViewModelBase类的示例:

public class ViewModelBase : BindableBase
{
      public virtual IDialogService DialogService 
      { 
         get { return AppContext.Current.DialogService; } 
      }
}

具体ViewModel的示例:

public class HomePageViewModel : ViewModelBase
{
      ...

      public void Cancel_CommandExecute()
      { 
         var dlgResult = DialogService.ShowMessageBox("Do you really want to discard unsaved changes?", "Confirm Exit", DialogButtons.YesNo);
         if (dlgResult != MessageBoxResult.Yes) return;
      }
}

对话服务:

public interface IDialogService
{
    MessageBoxResult ShowMessageBox(string messageBoxText, string caption = null, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.None);
}

public class DialogService : IDialogService
{
    public virtual MessageBoxResult ShowMessageBox(string messageBoxText, string caption = null, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.None)
    { 
       return MessageBox.Show(messageBoxText, caption, buttons, icon, defaultResult);
    }
}

在你的测试中,你可以模拟AppContext.Current,或者你可以覆盖ViewModelBase.DialogService属性。
也许这不是最干净的模拟DialogService的方式,但这是一种实用的方法。它使你的视图模型代码更加清晰、易读和可维护,因为你不需要在每个视图模型中注入和存储DialogService实例。你的视图模型仍然与视图解耦,可测试,可混合等。

基类应该继承自“BindableBase”吗?假设有人想要一个不包括Prism的简单情况呢? - mcy

1
您可以在代码后台定义并填充绑定。由于代码后台是视图的一部分,在其中调用消息框不会破坏MVVM模式。通过这种方式,您可以在设置绑定值之前显示确认对话框。
您需要在代码后台添加以下代码:
public partial class MainWindow : Window
{
    private DependencyProperty myDP;

    public MainWindow()
    {
        ...
        Binding myBinding = new Binding();
        myBinding.Path = new PropertyPath("myValue"); //myValue is a variable in ViewModel
        myBinding.Source = DataContext;
        myDP = DependencyProperty.Register("myValue", typeof(/*class or primitive type*/), typeof(MainWindow));
        BindingOperations.SetBinding(this, myDP, myBinding);
        ...
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        MessageBoxResult result = MessageBox.Show("Do you really want to do that?", "", MessageBoxButton.YesNo);
        if (result == MessageBoxResult.Yes
        {
            SetValue(myDP, /*value*/); //this sets the Binding value. myValue in ViewModel is set
        }
    }
}

当单击按钮时调用Button_Click方法,请添加:
Click="Button_Click"

将您的按钮的XAML定义。

  1. 我不能同意"ViewModel和View除了Bindings以外,不应该以任何其他方式进行通信"这个说法。在viewmodels中,你不应该创建views或访问它们,但你可以从views中访问viewmodels。
  2. 您的方法使得测试与对话框相关的逻辑变得不可能,例如viewmodel是否显示对话框或它如何对不同的答案做出反应。
- Liero

0

如果你想遵循MVVM模式,就不应该在你的视图模型中调用UI方法。

从你的视图模型打开/关闭窗口的正确方式是使用MVVMLight的Messanger或Prism的EventAggregator向你的视图发送消息。

EventAggregator允许你的视图模型向一组订阅者发送消息。当你订阅特定的消息时,你会附加一个要执行的函数。

我知道你正在学习这个模式的机制,所以你可以编写自己的EventAggregator并使用它。


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Philip Stuyck

0
我已经使用了MVVM Light messaging。PRISM库也提供了一种很好的方法来处理这个问题。
为了处理从视图模型触发的交互和从视图中的控件触发的交互,Prism库提供了InteractionRequests和InteractionRequestTriggers,以及自定义的InvokeCommandAction操作。InvokeCommandAction用于将包括事件在内的触发器连接到WPF命令。
在ViewModel中创建一个InteractionRequest属性:
public InteractionRequest<IConfirmation> ConfirmationRequest { get; private set; }

像这样调用交互:

private void RaiseConfirmation()
{
    this.ConfirmationRequest.Raise(
        new Confirmation { Content = "Confirmation Message", Title = "Confirmation" },
        c => { InteractionResultMessage = c.Confirmed ? "The user accepted." : "The user cancelled."; });
}

要使用交互请求,您需要在视图的XAML代码中定义相应的InteractionRequestTrigger:
<prism:InteractionRequestTrigger SourceObject="{Binding ConfirmationRequest, Mode=OneWay}">
    <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/>
</prism:InteractionRequestTrigger>

请查看 使用 Prism Library 5.0 for WPF 进行交互式快速入门


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接