首页 > 解决方案 > Caliburn Micro MVVM:处理从一个 View/ViewModel 到其他 View/ViewModel 的数据交换

问题描述

我创建了一个带有 2 个视图模型及其相关视图的 wpf 项目(使用 caliburn micro 和 MVVM 模式,没有代码隐藏):

ShellView 包含:

OtherView 包含一个 StackPanel,其中包含:

我的问题:

提前感谢您,如果需要,请随时修改下面的代码。

ShellView.xaml

<UserControl
    x:Class="CmMultipleViewModelView.Views.ShellView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid Width="800" Height="450">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <ContentControl
            x:Name="ActiveItem"
            Grid.Column="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center" />

        <TextBox
            x:Name="TargetText"
            Grid.Column="1"
            Width="80"
            HorizontalAlignment="Center"
            VerticalAlignment="Center" />
    </Grid>
</UserControl>

ShellViewModel.cs

public class ShellViewModel : Conductor<object>
{
    public ShellViewModel()
    {
        DisplayName = "Shell Window";
        var otherVM = new OtherViewModel();
        ActivateItem(otherVM);
    }
    public string DisplayName { get; set; }

    private string _targetText = "Target";
    public string TargetText
    {
        get => _targetText;
        set
        {
            _targetText = value; 
            NotifyOfPropertyChange(() => TargetText);
        }
    }
}

其他视图.xaml

<UserControl
    x:Class="CmMultipleViewModelView.Views.OtherView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="150"
    d:DesignWidth="150"
    mc:Ignorable="d">
    <StackPanel
        HorizontalAlignment="Center"
        VerticalAlignment="Top"
        Orientation="Vertical">
        <TextBox
            x:Name="SourceText"
            Width="80"
            Margin="3" />
        <Button
            x:Name="CopyText"
            Width="100"
            Margin="3"
            Content="Copy" />
    </StackPanel>
</UserControl>

其他ViewModel.cs

public class OtherViewModel : Screen
{
    private string _sourceText = "Source";
    public string SourceText
    {
        get => _sourceText;
        set
        {
            _sourceText = value; 
            NotifyOfPropertyChange(() => SourceText);
        }
    }

    public void CopyText()
    {
        // How to copy the SourceText to TargetText using Caliburn Micro MVVM?
        // Can ShellViewModel catch the PropertyChange event from source textbox?
    }
}

编辑:

AppBootstrapper.cs

public class AppBootstrapper : BootstrapperBase
{
    private readonly SimpleContainer _container = new SimpleContainer();

    public AppBootstrapper()
    {
        Initialize();
    }

    public ShellViewModel ShellViewModel { get; set; }

    protected override object GetInstance(Type serviceType, string key)
    {
        return _container.GetInstance(serviceType, key);
    }

    protected override IEnumerable<object> GetAllInstances(Type serviceType)
    {
        return _container.GetAllInstances(serviceType);
    }

    protected override void BuildUp(object instance)
    {
        _container.BuildUp(instance);
    }

    protected override void Configure()
    {
        base.Configure();
        _container.Singleton<IWindowManager, WindowManager>();
        _container.Singleton<IEventAggregator, EventAggregator>();
        _container.Singleton<ShellViewModel>();
        _container.PerRequest<OtherViewModel>(); // Or Singleton if there'll only ever be one

    }

    protected override void OnStartup(object sender, StartupEventArgs e)
    {
        try
        {
            base.OnStartup(sender, e);
            DisplayRootViewFor<ShellViewModel>();
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.StackTrace);
            Debug.WriteLine(ex.Message);
        }
    }
}

ShellViewModel.cs

public class ShellViewModel : Conductor<object>, IHandle<string>
{
    private readonly IEventAggregator _eventAggregator;

    public ShellViewModel(IEventAggregator eventAgg, OtherViewModel otherVm)
    {
        _eventAggregator = eventAgg;
        _eventAggregator.Subscribe(this);
        ActivateItem(otherVm);
    }

    public sealed override void ActivateItem(object item)
    {
        base.ActivateItem(item);
    }

    public OtherViewModel OtherViewModel { get; set; }

    private string _targetText = "Target";
    public string TargetText
    {
        get => _targetText;
        set
        {
            _targetText = value; 
            NotifyOfPropertyChange(() => TargetText);
        }
    }

    public void Handle(string message)
    {
        TargetText = message;
    }
}

其他ViewModel.cs

public class OtherViewModel : Screen
{
    private readonly IEventAggregator _eventAggregator;

    public OtherViewModel(IEventAggregator eventAgg)
    {
        _eventAggregator = eventAgg;
    }

    private string _sourceText = "Source";
    public string SourceText
    {
        get => _sourceText;
        set
        {
            _sourceText = value; 
            NotifyOfPropertyChange(() => SourceText);
        }
    }

    public void CopyText()
    {
        _eventAggregator.PublishOnUIThreadAsync(SourceText);
    }
}

再次编辑

添加

_container.Singleton<IWindowManager, WindowManager>();

在 AppBootstraper::Configure

问题解决了!

标签: c#wpfxamlmvvmcaliburn.micro

解决方案


正如其他人所说,正确的方法是使用事件聚合器。

如果您在 Caliburn.Micro 中使用 SimpleContainer,那么在您的 OnConfigure 覆盖中,您将输入:

_container.Singleton<IEventAggregator>();

这将在您第一次访问 IEventAggregator 时创建一个实例。现在,您可以选择如何访问它。通过注入构造函数或使用 IoC.GetInstance 方法。

如果要注入,则需要修改视图模型:

public class ShellViewModel : Conductor<object>, IHandle<string>
{
    private readonly IEventAggregator _eventAggregator;

    public ShellViewModel(IEventAggregator eventagg, OtherViewModel otherVM)
    {
        _eventAggregator = eventagg;
        _eventAggregator.Subscribe(this);
        ActivateItem(otherVM);
    }

    public void Handle(string message)
    {
        TargetText = message;
    }
}

public class OtherViewModel : Screen
{
    private readonly IEventAggregator _eventAggregator;

    public OtherViewModel(IEventAggregator eventagg)
    {
        _eventAggregator = eventagg;
    }

    public void CopyText()
    {
        _eventAggregator.PublishOnUIThread(SourceText);
    }
}

在 Bootstrapper 中,您需要注册两个视图模型:

_container.Singleton<ShellViewModel>();
_container.PerRequest<OtherViewModel>(); // Or Singleton if there'll only ever be one

那么,这一切在做什么呢?

在您的 ShellViewModel 中,我们告诉它为字符串实现 IHandle 接口。

IHandle<string>

任何时候触发字符串事件,ShellViewModel 都会调用具有相同签名的 Handle 方法。如果您只想处理特定类型,则创建一个新类来保存您的复制文本并将处理程序从字符串更改为您的类型。

IHandle<string>

IHandle<yourtype>

当事件聚合器接收到字符串事件时,它将调用任何侦听器的 Handle 方法。在您的情况下处理(字符串消息)。如果您更改 IHandle 类型,您还需要将 Handle 方法更改为相同的类型。

public void Handle(string message)
{
    TargetText = message;
}

这会将 TargetText 设置为您在事件中触发的任何字符串值。

我们有一个 IEventAggregator 实例,这是一个单例对象,因此在任何地方引用它都应该是同一个。我们已修改您的 ShellViewModel 构造函数以接受 IEventAggregator 对象和您的 OtherViewModel 的实例。

一旦我们在本地存储了对事件聚合器的引用,我们调用:

_eventAggregator.Subscribe(this);

这告诉事件聚合器我们对将由我们在类上定义的 IHandle 处理的任何事件感兴趣(只要它们处理不同的类型,您就可以拥有多个)。

与 OtherViewModel 有点不同,我们再次将 IEventAggregator 添加到构造函数中,以便我们可以在启动时注入它,但这次我们没有订阅任何事件,因为 OtherViewModel 只触发一个事件。

在您的 CopyText 方法中,您将调用:

_eventAggregator.PublishOnUIThread(SourceText);

这会在事件聚合器上引发事件。然后将其传播到使用 Handle 方法处理它的 ShellViewModel。

只要您在 Bootstrapper 的 SimpleContainer 实例中注册视图模型和事件聚合器,Caliburn.Micro 就会知道在创建 VM 实例时将哪些项目注入到构造函数中。

流程会:

ShellViewModel 订阅字符串事件

_eventAggregator.Subscribe(this);

用户在 SourceText 中键入一些文本用户按下鼠标右键,这会调用:

CopyText()

哪个电话:

_eventAggregator.PublishOnUIThread(SourceText);

事件聚合器然后检查所有具有 IHandle 接口的订阅视图模型,然后调用:

Handle(string message)

在每一个上。

在您的情况下,这会将 TargetText 设置为消息:

TargetText = message;

为文字墙道歉!

有一种更简单的方法是让您的 ShellViewModel 订阅 OtherViewModel 上的 PropertyChanged 事件:

otherVM.PropertyChange += OtherVMPropertyChanged;

然后在处理程序中,您必须查找 SourceText 属性的通知并更新您的目标文本。一个更简单的解决方案,但意味着您将 ShellVM 和 OtherVM 紧密耦合,而且您必须确保在关闭 OtherVM 时取消订阅该事件,否则它将永远不会被垃圾收集。

以下是设置 DI 容器的方法

在您的 Bootstrapper 类中,您需要添加 SimpleContainer:

private SimpleContainer _simplecontainer = new SimpleContainer();

然后你需要重写一些方法并确保代码如下:

protected override object GetInstance(Type serviceType, string key)
{
    return _container.GetInstance(serviceType, key);
}

protected override IEnumerable<object> GetAllInstances(Type serviceType)
{
    return _container.GetAllInstances(serviceType);
}

protected override void BuildUp(object instance)
{
    _container.BuildUp(instance);
}

现在覆盖 OnConfigure 方法。这是我们告诉 Caliburn.Micro 我们正在使用什么 ViewModel 以及我们设置 EventAggregator 和 WindowManager 的地方(因此它可以将您的 ShellViewModel 包装在一个窗口中):

protected override void Configure()
{
    base.Configure();

    _container.Singleton<IWindowManager, WindowManager>();
    _container.Singleton<IEventAggregator, EventAggregator>();

    _container.Singleton<ShellViewModel>();
    _container.PerRequest<OtherViewModel>(); // If you'll only ever have one OtherViewModel then you can set this as a Singleton instead of PerRequest
}

您的 DI 现已全部设置完毕。最后,在您的 StartUp 覆盖中,您只需确保它看起来像这样:

protected override void OnStartup(object sender, StartupEventArgs e)
{
    base.OnStartup(sender, e);

    DisplayRootViewFor<ShellViewModel>();
}

如果您现在运行您的应用程序,当创建 ShellViewModel 时,Caliburn.Micro 将查看 ShellViewModel 的构造函数参数以查看它需要提供什么。它将看到它需要一个事件聚合器和 OtherViewModel,因此它将查看 SimpleContainer 以查看它们是否已注册。如果他们有,那么它将创建实例(如果需要)并将它们注入到构造函数中。在创建 OtherViewModel 时,它还将检查构造函数参数并创建所需的任何内容。

最后它将显示 ShellViewModel。


推荐阅读