【问题标题】:What is the MVVM way to call WPF command from another viewmodel?从另一个视图模型调用 WPF 命令的 MVVM 方式是什么?
【发布时间】:2017-06-29 15:17:37
【问题描述】:

我正在尝试详细了解 WPF 中的 MVVM 实现,目前需要一些有关使用 ViewModel 进行导航的指导。我正在关注来自Rachel's blog 的 WPF 导航示例,并且需要一种从其他 ViewModel 调用 ApplicationViewModel 命令的方法。

根据博客,从 MainWindow 切换视图非常清楚,但我想了解更多关于视图间导航的信息,即说我在 MainWindow 上有 Home、Product 和 Contact 按钮以及 View 和 ViewModel 类,现在我想要从主页视图中的某个按钮而不是 MainWindow 打开联系人页面。我在 Home ViewModel 中编写了一些代码来实现相同的效果,但我怀疑这是否是 MVVM 的最佳实践。有什么方法可以从 HomeView.XAML 中实现同样的效果?

来自博客的代码片段 - ApplicationViewModel.cs

private ICommand _changePageCommand;

private IPageViewModel _currentPageViewModel;
private List<IPageViewModel> _pageViewModels;

public ApplicationViewModel()
{
    // Add available pages in c'tor
    PageViewModels.Add(new HomeViewModel(this));
    PageViewModels.Add(new ProductsViewModel());
    PageViewModels.Add(new ContactViewModel());
}

public ICommand ChangePageCommand
{
    get
    {
        if (_changePageCommand == null)
            _changePageCommand = new RelayCommand(
              p => ChangeViewModel((IPageViewModel)p), p => p is IPageViewModel);

        return _changePageCommand;
    }
}

private void ChangeViewModel(IPageViewModel viewModel)
{
    if (!PageViewModels.Contains(viewModel))
        PageViewModels.Add(viewModel);

    CurrentPageViewModel = PageViewModels.FirstOrDefault(vm => vm == viewModel);
}

来自博客的代码片段 - ApplicationView.xaml

<Window.Resources>
    <DataTemplate DataType="{x:Type local:HomeViewModel}">
        <local:HomeView />
    </DataTemplate>

    <!-- Data template for other views -->
</Window.Resources>

<DockPanel>
    <Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
        <ItemsControl ItemsSource="{Binding PageViewModels}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding Name}" 
                            Command="{Binding DataContext.ChangePageCommand, 
                            RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                            CommandParameter="{Binding }"/>

 <!--All closing tags-->

我在 HomeViewModel.cs 中的代码

// This is the command to get bind with my button inside Home view to invoke Contact view

private ICommand _loadContactCommand;
public ICommand LoadContactCommand
{
    get
    {
        if (_loadContactCommand == null)
            _loadContactCommand = new RelayCommand(p => LoadOtherView());

        return _loadContactCommand;
    }
}

private void LoadOtherView()
{
    // _appVM is the instance of 'ApplicationViewModel' which is being set from c'tor
    // Even I'm thinking to pass Contact view member of ApplicationViewModel class here, 
    // as I need exactly the same instance of the Contact which has been created earlier

    _appVM.ChangePageCommand.Execute(new ContactViewModel());
}

【问题讨论】:

  • 您可以使用中介模式。
  • @rory.ap 我了解中介者模式的基础知识。但是如何混合使用 MVVM 和 Mediator?

标签: c# wpf mvvm


【解决方案1】:

我有几种方法可以做到这一点。

第一个,如果动作是一种服务类型的交互,我认为这是一个相当好的例子,我会在接口中描述动作并将其作为依赖注入到需要它的 ViewModel。

这实际上就是您正在做的事情,但值得将其抽象为一个界面。这在两个 ViewModel 之间提供了不那么紧密的耦合。

下面是在 IPageDisplay 接口中封装功能的示例:

public interface IPageDisplay
{
    IPageViewModel GetCurrentPage();
    void ChangeViewModel(IPageViewModel newPage);
}

您的ApplicationViewModel 实现了它并且具有与之前完全相同的方法:

public class ApplicationViewModel: IPageDisplay
{
    // implement like you are doing

你是 HomeViewModel 然后将其作为接口,而不是“整个”视图模型:

class HomeViewModel
{
    HomeViewModel(IPageDisplay pageDisplay) {//constructor stuff}

private void LoadOtherView()
{
    // Instead of interacting with a whole ViewModel, we just use the interface
    _pageDisplay.ChangePageCommand.Execute(new ContactViewModel());
}

这更“安全”,因为它更抽象。您可以通过模拟IPageDisplay 来测试HomeViewModel 而无需创建AppViewModel。您可以更改页面的显示方式或AppViewModel 的实现,您还可以通过IPageDisplay 的其他实现来在任何其他类型的位置显示您的页面。

值得注意的是,任何需要执行导航操作的页面都需要IPageDisplay。如果你有很多依赖项,匹配所有这些依赖项可能会很麻烦 - 这就是依赖注入框架之类的东西可以真正提供帮助的地方。

第二个将是 cmets 中建议的中介模式。你可以有一个通用的调解器PageManager,它定义了ChangeViewModel(IPageViewModel newPage); 方法并触发ChangeViewModelRequest 事件或回调。 ApplicationViewModel 和任何其他想要更改当前页面的 ViewModels 接受 PageManager 实例作为依赖项。 ApplicationViewModel监听事件,对方调用ChangeViewModelRequest触发。

同样,如果依赖注入是在复杂的应用程序中,则需要对其进行有效管理。

这自然会导致第三个。它是中介者模式的扩展,一个事件聚合器。

事件聚合器是一种通用服务,它允许所有不同的 ViewModel 引发或订阅应用程序范围的事件。绝对值得一看。

在这里,您的 ApplicationViewModel 订阅了该事件:

public class ApplicationViewModel
{
    private EventAgregator _eventAggregator;

    ApplicationViewModel(EventAgregator eventAggregator)
    {
        this._eventAggregator = eventAggregator;
        _eventAggregator.Subscribe('ChangeViewModelRequest', (EventArgs eventArgs) => ChangeViewModel(eventArgs.Parameter))
    }

    private void ChangeViewModel(IPageViewModel viewModel)
    {
        if (!PageViewModels.Contains(viewModel))
            PageViewModels.Add(viewModel);

        CurrentPageViewModel = PageViewModels.FirstOrDefault(vm => vm == viewModel);
    }
}

并且 HomeViewModel 发布到事件:

private void LoadOtherView()
{
    _eventAggregator.Publish("ChangeViewModelRequest", new EventArgs(new ContactViewModel()));
}

您可以使用许多事件聚合器,其中一些内置于 MVVM 框架中,例如 Prism。

虽然与其他所有依赖项一样,这是一个依赖项 - 它是一个非常通用的依赖项。很有可能,您的大多数 ViewModel 都需要访问聚合器实例并将其作为依赖项,因为它几乎可以用于所有视图模型间的通信。简单地让所有虚拟机将其传递给构造函数中创建的任何虚拟机就可以用于简单的应用程序。但我还是想说支持依赖注入的东西(比如工厂模式?)值得实现。

编辑:

这是您的 HomeViewModel 所需的:

public class HomeViewModel : IPageViewModel // doesn't implement IPageDisplay
{
    private IPageDisplay _pageDisplay;
    public HomeViewModel(IPageDisplay pageDisplay)
    {
        // HomeViewModel doesn't implement IPageDisplay, it *consumes* one
        // as a dependency (instead of the previous ApplicationViewModel).
        // Note, that the instance you're passing still is the ApplicationViewModel,
        // so not much has actually changed - but it means you can have another
        // implementation of IPageDisplay. You're only linking the classes together
        // by the functionality of displaying a page.
        _pageDisplay= pageDisplay;
    }

    public string Name
    {
        get
        {
            return "Home Page";
        }
    }

    private ICommand _loadDashboardCommand;
    public ICommand LoadDashboardCommand
    {
        get
        {
            if (_loadDashboardCommand == null)
            {
                _loadDashboardCommand = new RelayCommand(
                    p => LoadOtherView());
            }
            return _loadDashboardCommand;
        }
    }

    private void LoadOtherView()
    {
        // Here you have the context of ApplicatiomViewModel like you required
        // but it can be replaced by any other implementation of IPageDisplay
        // as you're only linking the little bit of interface, not the whole class

        _pageDisplay.ChangeViewModel(new DashboardViewModel());
    }
}

}

【讨论】:

  • 似乎在一篇文章中有很多答案。我需要一些时间来深入理解你的帖子。完成后我会重新发布。
  • 任何问题/困惑问。我花了一段时间试图弄清楚如何构建 VM 和通信。这令人困惑。我尝试添加更多示例。
  • 正在尝试您的接口解决方案,我如何调用_pageDisplay.ChangePageCommand.Execute(new ContactViewModel());在ApplicationViewModel中定义的命令是_pageDisplay接口的一个实例?
  • 等等!抱歉,我没有正确阅读您的评论。忽略我的最后两个 cmets,我将删除它们。 ApplicationViewModel 是 _pageDisplay(它实现了 IPageDisplay 接口)。因此,这正是您当前拥有的方式,除了将接口添加到类声明中。当它想在命令中调用 ChangeViewModel() 时,它使用this.ChangeViewModel(...)。它不需要引用 _pageDisplay,因为它本身就是一个 IPageDisplay。所以不需要对当前的命令实现进行任何更改!
  • 我无法弄清楚这里的接口用法。请查看here 中的详细代码,并查看我在LoadOtherView() 中的最后一条评论。
猜你喜欢
  • 1970-01-01
  • 2010-12-26
  • 1970-01-01
  • 2010-11-28
  • 2014-10-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-17
相关资源
最近更新 更多