Caliburn Micro MVVM:处理从一个 View/ViewModel 到其他的数据交换
Caliburn Micro MVVM: Handling data exchange from one View/ViewModel to other ones
我创建了一个带有 2 个视图模型及其相关视图的 wpf 项目(使用带有 MVVM 模式的 Caliburn Micro,无代码隐藏):
- ShellView.xaml 和 ShellViewModel.cs
- OtherView.xaml 和 OtherViewModel.cs
ShellView 包含:
- ContentControl 指的是 OtherView/OtherViewModel.
- 一个文本框,其中包含所谓的"target text"。
OtherView 包含一个 StackPanel,其中包含:
- 一个接受用户文本的文本框(如"source text")。
- 将源文本复制到鼠标右键单击事件的目标的按钮。
我的问题:
- 如何将OtherView/ViewModel中的源文本复制到ShellView/ViewModel中的目标文本?有什么最佳实践吗?
- ShellViewModel 能否从源 TextBox 捕获 PropertyChange 事件?
- 如何反向复制(从目标到源)?
在此先感谢您,如果需要,请随时修改下面的代码。
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。
我创建了一个带有 2 个视图模型及其相关视图的 wpf 项目(使用带有 MVVM 模式的 Caliburn Micro,无代码隐藏):
- ShellView.xaml 和 ShellViewModel.cs
- OtherView.xaml 和 OtherViewModel.cs
ShellView 包含:
- ContentControl 指的是 OtherView/OtherViewModel.
- 一个文本框,其中包含所谓的"target text"。
OtherView 包含一个 StackPanel,其中包含:
- 一个接受用户文本的文本框(如"source text")。
- 将源文本复制到鼠标右键单击事件的目标的按钮。
我的问题:
- 如何将OtherView/ViewModel中的源文本复制到ShellView/ViewModel中的目标文本?有什么最佳实践吗?
- ShellViewModel 能否从源 TextBox 捕获 PropertyChange 事件?
- 如何反向复制(从目标到源)?
在此先感谢您,如果需要,请随时修改下面的代码。
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。