【问题标题】:Who owns the Message Box, the View or the ViewModel?谁拥有消息框、视图或视图模型?
【发布时间】:2018-07-27 21:59:48
【问题描述】:

我正在使用 Mahapps.Metro 开发一个 WPF 应用程序,这是一个提供“现代”UI 样式的 Nuget 包。

我创建了一个对话框,它是您在左侧选择一个项目,单击右箭头按钮,该项目移动到右侧的选择对话框之一。

我的对话框中的一个验证规则是,在您按下按钮之前必须至少选择一个项目,因此(在我的视图背后的代码中)我打开一个消息框并通知用户,如果他没有t 至少选择一项:

private void AddButton_Click(object sender, RoutedEventArgs e)
{
    if (!IsAnySelected(Users))
    {
        // MetroWindow call
        this.ShowMessageAsync("Permissions", "Please select a User.");
        return;

        // Call ViewModel `AddPermissions()` method here.
    }
}

bool IsAnySelected(DataGrid dataGrid)
{
    foreach(dynamic d in dataGrid.ItemsSource)
    { 
        if (d.IsSelected) return true;
    }
    return false;
}

DataGrid 绑定到 ViewModel 中的 ObservableCollection

由于 WPF 中的普通消息框无法设置样式,因此 Mahapps 提供了自己的样式。正是在这里,我发现当我尝试在视图中打开消息框时,MahApps 会引发空引用异常。这与我的设置有关,因为他们的演示工作正常。

原来有人提供了一种打开Mahapps消息框in the View Model instead of the view.的方法我的问题是,你为什么要这样做?

View 不对任何视觉元素(包括消息框)负责吗?验证不是 View 的 Code Behind 中允许做的一件事吗?

请注意,这种方法会导致新的问题,即您现在需要一种方法来fire the View Model's method or ICommand from CodeBehind

(DataContext as SecurityDialogViewModel).AddPermissions();

【问题讨论】:

  • 打开消息框类似于发出CommandCommand 将由 ViewModel 处理。在这种情况下,该命令用于打开对话框。这没什么错,因为这是 ViewModel 的责任。
  • @CodingYoshi:当然可以,但是出于验证目的,我无法访问 ViewModel 中的表单控件。相反,我将不得不依赖 View Model 的数据绑定 ObservableCollection 中的 IsSelected 字段的值(这是一种可行的,如果迂回的方法,但我的直觉仍然认为 ViewModel 没有业务站立 UI成分)。 Mahapps 人在 ViewModel 中提出的用于执行此操作的技术将 Mahapps.Metro 与 ViewModel 紧密绑定,对此我也不感到兴奋。
  • 即使您可以访问控件,在 VM 中使用它也不是一个好主意,因为您的 VM 会与 UI 元素耦合。换句话说,您将无法对其进行单元测试。所以使用IsSelected 非常好。虚拟机协调工作,所以我认为如果它是站立的 UI 组件很好(你可以在两者之间创建另一个项目并将其称为协调器或其他东西,但我不会)。对于模式,讨论可以来回进行,争论的任何一方都会有优势。如果我在 VM 中执行此操作,我将确保在所有屏幕上保持一致。
  • 消息框一个 UI 元素。如果视图模型正忙于打开模式消息框,你将如何对它进行单元测试?
  • 这不是意味着 ViewModel 现在必须知道 View 吗?

标签: c# wpf mvvm mahapps.metro


【解决方案1】:

您想要做的与启用按钮并在单击时保存某些内容没有什么不同。在您的情况下,它正在启用对话框并显示它。挑战在于如何在不耦合到对话窗口的情况下从 ViewModel 执行此操作。

您需要注意两件事:

  1. 可以显示对话框吗?
  2. 显示对话框

在您的 ViewModel 中,您可以这样做:

public class MainViewModel
{
    private IDialog dialog;
    private ICommand showCommand;
    public MainViewModel() : this(null)
    {
    }

    public MainViewModel(IDialog dialog)
    {
        this.dialog = dialog;
        this.showCommand = new ShowCommand(this.ShowCommandHandler);
    }

    private void ShowCommandHandler(object sender, EventArgs e)
    {
        this.dialog.Show();
    }

    public ICommand ShowCommand { get { return this.showCommand; } }
}

请注意上面的ShowCommandHandler 和构造函数重载,它采用IDialog。这里是IDialog

public interface IDialog
{
    void Show();
}

在我的示例中,IDialog 已显示,但在您的情况下,它将是一些显示滑动对话框的方法。如果是Show,那就太好了!否则更改它,使其与对话框中的方法相匹配。

命令如下:

public class ShowCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    public EventHandler handler;
    public ShowCommand(EventHandler handler)
    {
        this.handler = handler;
    }

    public bool CanExecute(object parameter)
    {
        // Decide whether this command can be executed. 
        // Check if anything is selected from within your collection
        throw new NotImplementedException();
    }

    public void Execute(object parameter)
    {
        this.handler(this, EventArgs.Empty);
    }
}

最后在你的主视图中,这样做:

public partial class MainView : Window
{
    public MainView()
    {
        this.InitializeComponent();
        this.DataContext = new MainViewModel(/*pass your dialog here*/);
    }
}

请注意,这与您在视图中保存Button 并且按钮的Enabled 属性将在命令中绑定到IsEnabled 没有什么不同。然后一旦启用按钮,用户单击它并保存对象。在这种情况下,它的滑动能力(或显示能力)是绑定的,但不是保存,而是简单地调用IDialog 上的Show

【讨论】:

    【解决方案2】:

    您尝试完成的所有事情都可以在视图中完成,无需考虑 ViewModel,除非您有更高级的要求。 ShowMessageAsync 方法是异步的,但您没有异步方法签名,并且您不等待返回。

    您也可以完全摆脱 IsAnySelected 方法而只使用 Linq。下面:

    private async void AddButton_Click(object sender, RoutedEventArgs e)
    {
    
    var IsAnySelectedUsers = dataGrid.ItemsSource.Cast<User>().Any(p => p.IsSelected);
    
    if (!IsAnySelectedUsers)
    {
        await this.ShowMessageAsync("Permissions", "Please select a User.");
    }
    

    }

    我正在使用 Linq 查询 Datagrid ItemsSource(我的 datagrid 正在使用 User 的集合,所以如果这不是您的基础类,您需要将其替换为用于您的 datagrid 的任何基础类)

    【讨论】:

    • 嗯,差不多。正如我在问题中所描述的,一旦验证成功,我打算将控制权传递给我的 ViewModel 中的方法。
    【解决方案3】:

    我一直采用通过回调接口公开用户对话框的方法。 OpenFileDialog、SaveFileDialog、MessageBox、FolderSelectionDialog等由一个接口定义:

    public interface IMainViewCallbacks
    {
            bool GetPathViaOpenDialog(out string filePath, string szFilter, 
                     string szDefaultExt, string szInitialDir);
            bool GetPathViaSaveDialog(out string filePath, string szFilter, 
                     string szDefaultExt, string szInitialDir);
            bool GetFolderPath(out string folderPath);
            MessageBoxResult MessageBox(string messageBoxText, string caption, 
                      MessageBoxButton button, MessageBoxImage icon);
    }
    

    然后,在视图代码隐藏中实现接口。

    在view ctor中创建viewmodel时,传递this:

    public class MainViewMode : IMainViewCallbacks
    {
       private vm = null;
       public MainWindow()
       {
          vm = new MainViewModel(this);
          this.DataContext = vm;
       }
    }
    

    最后,为你的 viewmodel ctor 添加一个参数来接收接口:

    public class MainViewModel
    {
        IMainViewCallbacks Calllbacks = null;
        public MainViewModel(IMainViewCallbacks cb)
        {
           // stash the callbacks for later.
           this.Callbacks = cb;
        }
    
        // pseudocode for the command that consumes the callback
        public ICommand .... 
        {
            Execute() { this.Callbacks.GetPathViaOpenDialog(); }
        } 
    }
    

    这是可单元测试的;单元测试视图提供的接口可以假装接收到用户输入,只返回一个常量值。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多