在 Caliburn.Micro 中管理 ViewModel
Managing ViewModels in Caliburn.Micro
我正在使用 Caliburn.Micro 框架将正在开发的应用程序更改为 MVVM 模式。
随着我逐渐习惯这一点,起初,我使用 IConductor
界面通过在 MainViewModel 继承 Conductor<object>
进行导航,然后使用 ActivateItem 方法导航 Screens。
我没有使用容器,而是每次都实例化一个新的 ViewModel。
例如,要导航到 FirstViewModel,我使用的是 ActivateItem(new FirstViewModel());
ViewModelels 占用资源少,因此此实现并不引人注目。但是,我发现 ViewModel 实例没有被处理,我已经开始使用定时器来检查实例是否仍然 运行,在后台堆积。
从那时起,我尝试了各种实现来控制 ViewModel 的管理方式。
我想要的是能够决定我是引用一个已经实例化的 ViewModel 还是实例化一个新的。
另外,我想决定是处置 ViewModel 还是保留它 运行 以便稍后重新连接它。
所以,阅读文档,我在 BootStrapperBase 中实现了一个 SimpleContainer
public class Bootstrapper : BootstrapperBase
{
private SimpleContainer _container = new SimpleContainer();
public Bootstrapper()
{
Initialize();
}
protected override void Configure()
{
_container.Instance(_container);
_container
.Singleton<IWindowManager, WindowManager>()
.Singleton<IEventAggregator, EventAggregator>();
GetType().Assembly.GetTypes()
.Where(type => type.IsClass)
.Where(type => type.Name.EndsWith("ViewModel"))
.ToList()
.ForEach(viewModelType => _container.RegisterPerRequest(viewModelType, viewModelType.ToString(), viewModelType));
}
protected override object GetInstance(Type service, string key)
{
var instance = _container.GetInstance(service, key);
if (instance != null)
return instance;
throw new InvalidOperationException("Could not locate any instances.");
}
protected override IEnumerable<object> GetAllInstances(Type service)
{
return _container.GetAllInstances(service);
}
protected override void BuildUp(object instance)
{
_container.BuildUp(instance);
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<ShellViewModel>();
}
}
我认为 IoC.Get<FirstViewModel>()
会实例化一个新的 ViewModel 或重用一个打开的 ViewModel,如果它已经被实例化的话。
但是,它每次都在实例化一个新的 ViewModel。
此外,我不知道如何在激活另一个 ViewModel 时处理它。
例如,我在切换到另一个 ViewModel 时触发的 FirstViewModel 上放置了一个 OnDeactivate,但我不知道应该放什么代码来处理该实例。
我已经尝试过此设置,实现了 IDisposable 接口,但我收到了 System.WhosebugException.
protected override void OnDeactivate(bool close)
{
Dispose();
Console.WriteLine("deactivated");
}
public void Dispose()
{
base.TryClose();
}
Caliburn.Micro 中的 SimpleContainer 是否不足以管理 ViewModel,或者我应该研究不同的方法吗?
我知道我似乎在问多个问题,但所有这些问题都是关于管理视图模型的主要问题。
阅读文档时我遇到了 Lifecycle
概念,我认为这个概念可以解决我的问题,但我没有找到进一步的解释。
Caliburn.Micro 上的文档没有给出很多例子,我发现没有例子很难理解如何正确使用这个框架。
SimpleContainer 中的 RegisterSingleton 将完成这项工作...
因此,如果您想根据自己的选择进行实例化,您可以使用一个助手来检查类型的构造函数及其默认参数:(一些反射知识)在您可以调整代码之后..
但如果您觉得太复杂,请先查看 Activator.Createinstance。
public static class HelperConstructor
{
public static T MyCreateInstance<T>()
where T : class
{
return (T) MyCreateInstance(typeof (T));
}
public static object MyCreateInstance(Type type)
{
var ctor = type
.GetConstructors()
.FirstOrDefault(c => c.GetParameters().Length > 0);
return ctor != null
? ctor.Invoke
(ctor.GetParameters()
.Select(p =>
p.HasDefaultValue? p.DefaultValue :
p.ParameterType.IsValueType && Nullable.GetUnderlyingType(p.ParameterType) == null
? Activator.CreateInstance(p.ParameterType)
: null
).ToArray()
)
: Activator.CreateInstance(type);
}
}
您可以通过提供类型来使用此助手:
var instanceviewModel = HelperConstructor.MyCreateInstance(classType);
如果需要,稍后 caliburn 会自动创建视图实例...
您看 IConductor
是对的,这是 Caliburn 希望我们用来管理组件生命周期的东西。为了完整起见,还有 ActivateWith
、DeactivateWith
和 ConductWith
扩展方法 link Screen
生命周期没有 Conductor
的干预,但我倾向于避开那些。尽管我可能会在异国情调的单元测试场景中使用它们。
如文档中所述,停用可以有多种含义。让我们以 TabControl
为例结合 Conductor<IScreen>.Collection.OneActive
.
- 我们可以从一个选项卡切换到另一个选项卡。我们不想关闭我们开始的选项卡,我们只想停用它。
- 我们可以关闭当前选项卡,切换到(激活)上一个索引处的选项卡(Caliburn 的默认设置)。
由于这种灵活性,即多种可能性,Caliburn 不会将任何一种行为强加给您。当然,这意味着您必须自己进行适当的调用。
第一种情况很简单,分配一个新的 ActiveItem
会自动停用前一个。
第二种情况要求您明确关闭标签页。但是,这将触发 Caliburn 分配一个新的 ActiveItem
。您可以使用默认策略,或实施您自己的策略,或者您可以确保该项目在关闭后不再处于活动状态。在那种情况下,Caliburn 不需要寻找其他地方。
本文中值得注意的扩展方法在 ScreenExtensions.cs 中定义。
关闭项目的最简单方法是 await conductor.TryCloseAsync(item)
和可选的 CancellationToken
。此方法仅转发到 conductor.DeactivateItemAsync(item, true, CancellationToken.None);
.
在 Conductor<IScreen>.Collection.OneActive
的情况下,接下来给出 implementation。
/// <summary>
/// Deactivates the specified item.
/// </summary>
/// <param name="item">The item to close.</param>
/// <param name="close">Indicates whether or not to close the item after deactivating it.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public override async Task DeactivateItemAsync(T item, bool close, CancellationToken cancellationToken = default)
{
if (item == null)
return;
if (!close)
await ScreenExtensions.TryDeactivateAsync(item, false, cancellationToken);
else
{
var closeResult = await CloseStrategy.ExecuteAsync(new[] { item }, CancellationToken.None);
if (closeResult.CloseCanOccur)
await CloseItemCoreAsync(item, cancellationToken);
}
}
一旦您知道要查看的位置,这一切都将不言自明。 close
标志是停用和关闭项目之间的区别。 CloseStrategy
是 Caliburn 启用正常关机的方式,例如 "Are you sure you want to close the item?"。 CloseItemCoreAsync
接下来在源文件中实现,随意有一个 look. The ScreenExtensions.TryDeactivateAsync
used in either branch will eventually forward to DeactivateAsync
on the screen 本身,它负责清理。
回到您的用例,当您指示从一个项目导航到下一个项目时,可以选择切换回内存中的现有实例,我建议您使用 Conductor<IScreen>.Collection.OneActive
。然后您可以查询它的 Items
集合以查明某个实例是否已经存在,以便激活它或创建一个新实例。
综上所述,激活和停用最好通过导体完成。
如果您需要明确处理,您可以将示例更改为以下示例。
protected override void OnDeactivate(bool close)
{
if (close)
{
Dispose();
}
}
public void Dispose()
{
Console.WriteLine("disposed");
}
然而,在 Dispose
中调用 base.TryClose();
是不必要的,并且会导致 OnDeactivate
和 TryClose
之间的无限循环。 Dispose
模式仅在清理非托管资源时才需要,例如文件句柄、ref MSDN.
更新
Using Conductor.Collection.OneActive is not closing the the ViewModel but then, when I use ActivateItem(IoC.Get());, the ViewModel is created again because I see how it runs the constructor again. I am missing something.
就我个人而言,我是 the pit of success 的坚定支持者,当设计良好的框架(例如 Caliburn)公开静态服务定位器时,我总是觉得有些失望。当我们陷入困境时,我们很容易被引诱到黑暗面。
如前所述:
You could then query its Items
collection to find out if a certain instance already exists, in order to activate it or create a new one.
要找出某个实例是否已经存在,我们需要一种方法来识别它。它可以基于类型,但为了简单起见,我们使用 int Id
属性。假设 Items
集合中的所有(或部分)视图模型都装饰有 IHasEntity
接口(暴露了 Id
属性),我们正在寻找 Id == 3
.
在指挥的范围内,您需要做的就是:
var match = Items.OfType<IHasEntity>().FirstOrDefault(vm => vm.Id == 3);
if (match != null) // Activate it
{
ActiveItem = match;
}
else // Create a new instance
{
var entity = await _repo.GetId(3);
ActiveItem = new MyViewModel(entity);
}
结束思考,如果你所有的视图模型都实现了通用的 IHasEntity
抽象,你可以将你的导体定义为 Conductor<IHasEntity>.Collection.OneActive
并且不再需要 .OfType<IHasEntity>()
过滤器。
我正在使用 Caliburn.Micro 框架将正在开发的应用程序更改为 MVVM 模式。
随着我逐渐习惯这一点,起初,我使用 IConductor
界面通过在 MainViewModel 继承 Conductor<object>
进行导航,然后使用 ActivateItem 方法导航 Screens。
我没有使用容器,而是每次都实例化一个新的 ViewModel。
例如,要导航到 FirstViewModel,我使用的是 ActivateItem(new FirstViewModel());
ViewModelels 占用资源少,因此此实现并不引人注目。但是,我发现 ViewModel 实例没有被处理,我已经开始使用定时器来检查实例是否仍然 运行,在后台堆积。
从那时起,我尝试了各种实现来控制 ViewModel 的管理方式。 我想要的是能够决定我是引用一个已经实例化的 ViewModel 还是实例化一个新的。 另外,我想决定是处置 ViewModel 还是保留它 运行 以便稍后重新连接它。
所以,阅读文档,我在 BootStrapperBase 中实现了一个 SimpleContainer
public class Bootstrapper : BootstrapperBase
{
private SimpleContainer _container = new SimpleContainer();
public Bootstrapper()
{
Initialize();
}
protected override void Configure()
{
_container.Instance(_container);
_container
.Singleton<IWindowManager, WindowManager>()
.Singleton<IEventAggregator, EventAggregator>();
GetType().Assembly.GetTypes()
.Where(type => type.IsClass)
.Where(type => type.Name.EndsWith("ViewModel"))
.ToList()
.ForEach(viewModelType => _container.RegisterPerRequest(viewModelType, viewModelType.ToString(), viewModelType));
}
protected override object GetInstance(Type service, string key)
{
var instance = _container.GetInstance(service, key);
if (instance != null)
return instance;
throw new InvalidOperationException("Could not locate any instances.");
}
protected override IEnumerable<object> GetAllInstances(Type service)
{
return _container.GetAllInstances(service);
}
protected override void BuildUp(object instance)
{
_container.BuildUp(instance);
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<ShellViewModel>();
}
}
我认为 IoC.Get<FirstViewModel>()
会实例化一个新的 ViewModel 或重用一个打开的 ViewModel,如果它已经被实例化的话。
但是,它每次都在实例化一个新的 ViewModel。
此外,我不知道如何在激活另一个 ViewModel 时处理它。 例如,我在切换到另一个 ViewModel 时触发的 FirstViewModel 上放置了一个 OnDeactivate,但我不知道应该放什么代码来处理该实例。 我已经尝试过此设置,实现了 IDisposable 接口,但我收到了 System.WhosebugException.
protected override void OnDeactivate(bool close)
{
Dispose();
Console.WriteLine("deactivated");
}
public void Dispose()
{
base.TryClose();
}
Caliburn.Micro 中的 SimpleContainer 是否不足以管理 ViewModel,或者我应该研究不同的方法吗?
我知道我似乎在问多个问题,但所有这些问题都是关于管理视图模型的主要问题。
阅读文档时我遇到了 Lifecycle
概念,我认为这个概念可以解决我的问题,但我没有找到进一步的解释。
Caliburn.Micro 上的文档没有给出很多例子,我发现没有例子很难理解如何正确使用这个框架。
SimpleContainer 中的 RegisterSingleton 将完成这项工作...
因此,如果您想根据自己的选择进行实例化,您可以使用一个助手来检查类型的构造函数及其默认参数:(一些反射知识)在您可以调整代码之后..
但如果您觉得太复杂,请先查看 Activator.Createinstance。
public static class HelperConstructor
{
public static T MyCreateInstance<T>()
where T : class
{
return (T) MyCreateInstance(typeof (T));
}
public static object MyCreateInstance(Type type)
{
var ctor = type
.GetConstructors()
.FirstOrDefault(c => c.GetParameters().Length > 0);
return ctor != null
? ctor.Invoke
(ctor.GetParameters()
.Select(p =>
p.HasDefaultValue? p.DefaultValue :
p.ParameterType.IsValueType && Nullable.GetUnderlyingType(p.ParameterType) == null
? Activator.CreateInstance(p.ParameterType)
: null
).ToArray()
)
: Activator.CreateInstance(type);
}
}
您可以通过提供类型来使用此助手:
var instanceviewModel = HelperConstructor.MyCreateInstance(classType);
如果需要,稍后 caliburn 会自动创建视图实例...
您看 IConductor
是对的,这是 Caliburn 希望我们用来管理组件生命周期的东西。为了完整起见,还有 ActivateWith
、DeactivateWith
和 ConductWith
扩展方法 link Screen
生命周期没有 Conductor
的干预,但我倾向于避开那些。尽管我可能会在异国情调的单元测试场景中使用它们。
如文档中所述,停用可以有多种含义。让我们以 TabControl
为例结合 Conductor<IScreen>.Collection.OneActive
.
- 我们可以从一个选项卡切换到另一个选项卡。我们不想关闭我们开始的选项卡,我们只想停用它。
- 我们可以关闭当前选项卡,切换到(激活)上一个索引处的选项卡(Caliburn 的默认设置)。
由于这种灵活性,即多种可能性,Caliburn 不会将任何一种行为强加给您。当然,这意味着您必须自己进行适当的调用。
第一种情况很简单,分配一个新的 ActiveItem
会自动停用前一个。
第二种情况要求您明确关闭标签页。但是,这将触发 Caliburn 分配一个新的 ActiveItem
。您可以使用默认策略,或实施您自己的策略,或者您可以确保该项目在关闭后不再处于活动状态。在那种情况下,Caliburn 不需要寻找其他地方。
本文中值得注意的扩展方法在 ScreenExtensions.cs 中定义。
关闭项目的最简单方法是 await conductor.TryCloseAsync(item)
和可选的 CancellationToken
。此方法仅转发到 conductor.DeactivateItemAsync(item, true, CancellationToken.None);
.
在 Conductor<IScreen>.Collection.OneActive
的情况下,接下来给出 implementation。
/// <summary>
/// Deactivates the specified item.
/// </summary>
/// <param name="item">The item to close.</param>
/// <param name="close">Indicates whether or not to close the item after deactivating it.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public override async Task DeactivateItemAsync(T item, bool close, CancellationToken cancellationToken = default)
{
if (item == null)
return;
if (!close)
await ScreenExtensions.TryDeactivateAsync(item, false, cancellationToken);
else
{
var closeResult = await CloseStrategy.ExecuteAsync(new[] { item }, CancellationToken.None);
if (closeResult.CloseCanOccur)
await CloseItemCoreAsync(item, cancellationToken);
}
}
一旦您知道要查看的位置,这一切都将不言自明。 close
标志是停用和关闭项目之间的区别。 CloseStrategy
是 Caliburn 启用正常关机的方式,例如 "Are you sure you want to close the item?"。 CloseItemCoreAsync
接下来在源文件中实现,随意有一个 look. The ScreenExtensions.TryDeactivateAsync
used in either branch will eventually forward to DeactivateAsync
on the screen 本身,它负责清理。
回到您的用例,当您指示从一个项目导航到下一个项目时,可以选择切换回内存中的现有实例,我建议您使用 Conductor<IScreen>.Collection.OneActive
。然后您可以查询它的 Items
集合以查明某个实例是否已经存在,以便激活它或创建一个新实例。
综上所述,激活和停用最好通过导体完成。
如果您需要明确处理,您可以将示例更改为以下示例。
protected override void OnDeactivate(bool close)
{
if (close)
{
Dispose();
}
}
public void Dispose()
{
Console.WriteLine("disposed");
}
然而,在 Dispose
中调用 base.TryClose();
是不必要的,并且会导致 OnDeactivate
和 TryClose
之间的无限循环。 Dispose
模式仅在清理非托管资源时才需要,例如文件句柄、ref MSDN.
更新
Using Conductor.Collection.OneActive is not closing the the ViewModel but then, when I use ActivateItem(IoC.Get());, the ViewModel is created again because I see how it runs the constructor again. I am missing something.
就我个人而言,我是 the pit of success 的坚定支持者,当设计良好的框架(例如 Caliburn)公开静态服务定位器时,我总是觉得有些失望。当我们陷入困境时,我们很容易被引诱到黑暗面。
如前所述:
You could then query its
Items
collection to find out if a certain instance already exists, in order to activate it or create a new one.
要找出某个实例是否已经存在,我们需要一种方法来识别它。它可以基于类型,但为了简单起见,我们使用 int Id
属性。假设 Items
集合中的所有(或部分)视图模型都装饰有 IHasEntity
接口(暴露了 Id
属性),我们正在寻找 Id == 3
.
在指挥的范围内,您需要做的就是:
var match = Items.OfType<IHasEntity>().FirstOrDefault(vm => vm.Id == 3);
if (match != null) // Activate it
{
ActiveItem = match;
}
else // Create a new instance
{
var entity = await _repo.GetId(3);
ActiveItem = new MyViewModel(entity);
}
结束思考,如果你所有的视图模型都实现了通用的 IHasEntity
抽象,你可以将你的导体定义为 Conductor<IHasEntity>.Collection.OneActive
并且不再需要 .OfType<IHasEntity>()
过滤器。