Caliburn Micro MVVM:处理从一个 View/ViewModel 到其他的数据交换

Caliburn Micro MVVM: Handling data exchange from one View/ViewModel to other ones

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

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);
        }
    }
}

OtherView.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>

OtherViewModel.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;
    }
}

OtherViewModel.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

问题解决了!

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

如果您在 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 方法。如果您只想处理特定类型,则创建一个新的 class 来保存您的副本文本并将处理程序从字符串更改为您的类型。

IHandle<string>

IHandle<yourtype>

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

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

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

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

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

_eventAggregator.Subscribe(this);

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

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

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

_eventAggregator.PublishOnUIThread(SourceText);

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

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

流向:

ShellViewModel 订阅字符串事件

_eventAggregator.Subscribe(这个);

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

CopyText()

调用:

_eventAggregator.PublishOnUIThread(SourceText);

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

Handle(string message)

每一个。

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

TargetText = message;

对文字墙深表歉意!

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

otherVM.PropertyChange += OtherVMPropertyChanged;

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

以下是设置 DI 容器的方法

在您的 Bootstrapper class 中,您需要添加 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 包装在 window 中):

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。