在 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 希望我们用来管理组件生命周期的东西。为了完整起见,还有 ActivateWithDeactivateWithConductWith 扩展方法 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(); 是不必要的,并且会导致 OnDeactivateTryClose 之间的无限循环。 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>() 过滤器。