【问题标题】:Managing ViewModels in Caliburn.Micro在 Caliburn.Micro 中管理 ViewModel
【发布时间】:2020-05-01 10:38:50
【问题描述】:

我正在使用 Caliburn.Micro 框架将我正在开发的应用程序更改为 MVVM 模式。

当我习惯了这一点时,起初,我使用 IConductor 接口进行导航,方法是在 MainViewModel 继承 Conductor<object>,然后使用 ActivateItem 方法导航 Screens。

我没有使用容器,而是每次都实例化一个新的 ViewModel。

例如,要导航到 FirstViewModel,我使用的是ActivateItem(new FirstViewModel());

ViewModelel 资源较少,因此这种实现并不引人注目。但是,我发现 ViewModel 实例没有被释放,我已经开始使用 Timers 来检查实例是否仍在运行,并在后台堆积。

从那时起,我尝试了各种实现来控制 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&lt;FirstViewModel&gt;() 会实例化一个新的 ViewModel 或重用一个打开的 ViewModel,如果它已经实例化的话。 但是,它每次都在实例化一个新的 ViewModel。

另外,我无法弄清楚在激活另一个 ViewModel 时如何处理 ViewModel。 例如,我在 FirstViewModel 上放置了一个 OnDeactivate,它在切换到另一个 ViewModel 时触发,但我不知道应该放什么代码来处理该实例。 我已经尝试过这个安装程序,实现了 IDisposable 接口,但我收到了 System.StackOverflowException。

protected override void OnDeactivate(bool close)
        {

            Dispose();
            Console.WriteLine("deactivated");
        }
public void Dispose()
        {
            base.TryClose();
        }

Caliburn.Micro 的 SimpleContainer 还不足以管理 ViewModel,还是我应该研究一种不同的方法?

我知道我似乎在问多个问题,但所有这些问题都与管理视图模型有关的主要问题有关。

阅读文档时,我遇到了 Lifecycle 概念,我认为这个概念可以解决我的问题,但我没有找到进一步的解释。

Caliburn.Micro 上的文档没有提供很多示例,我发现如果没有示例,我很难理解如何正确使用这个框架。

【问题讨论】:

  • 如果我理解您只想要一个 ViewModel 实例?如果是这样,为什么不使用 RegisterSingleton 而不是 RegisterPerRequest ?另一件事..您谈到了未使用的实例的处置... GC 完成了它的工作...只需知道您是否将 EventAggregator 与 Caliburn 一起使用,不要忘记取消订阅..仅此而已...视图和链接的视图模型将是垃圾..您可以测试:只需添加析构函数并设置断点
  • 只有在必要时垃圾收集器才会做一件事......所以在内存释放之前的时间可能很长......在你总是可以用代码激活 GC 之后
  • 实际上,我使用System.Windows.Forms.Timer 来检查该类是否已被销毁,但这样可以使其保持活动状态
  • @Frenchy,我只是在不知道具体细节的情况下复制/粘贴了引导程序配置。那么,RegisterPerRequest 总是创建一个新实例,而 RegisterSingleton 只创建一个实例?
  • 在您的 cmets 之后,我重新阅读了有关 SimpleContainer 的文档,它更容易理解

标签: c# wpf mvvm caliburn.micro


【解决方案1】:

您对IConductor 的看法是正确的,这是 Caliburn 期​​望我们用来管理组件生命周期的。为了完整起见,还有ActivateWithDeactivateWithConductWith 扩展方法来链接Screen 生命周期而无需Conductor 的干预,但我倾向于避开这些。虽然我可能会在奇异的单元测试场景中使用它们。

如文档中所述,停用可以有多种含义。我们以TabControl 为例,结合Conductor&lt;IScreen&gt;.Collection.OneActive

  • 我们可以从一个选项卡切换到另一个选项卡。我们不想关闭我们开始时使用的选项卡,我们只想停用它。
  • 我们可以关闭当前选项卡,切换到(激活)上一个索引处的选项卡(Caliburn 的默认设置)。

由于这种灵活性,即多种可能性,Caliburn 不会将任何一种行为强加给您。当然,这意味着您必须自己进行适当的调用。

第一种情况很简单,分配一个新的ActiveItem 会自动停用前一个。

第二种情况要求您明确关闭选项卡。但是,这将触发 Caliburn 分配一个新的ActiveItem。您可以使用默认策略,或实施您自己的策略,或者您可以确保该项目在您关闭后不再是活动的。在这种情况下,Caliburn 不需要寻找其他地方。

此上下文中值得注意的扩展方法在ScreenExtensions.cs 中定义。

关闭项目的最简单方法是await conductor.TryCloseAsync(item) 和可选的CancellationToken。此方法仅转发到conductor.DeactivateItemAsync(item, true, CancellationToken.None);

如果是Conductor&lt;IScreen&gt;.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 启用正常关闭的方式,例如 “您确定要关闭该项目吗?”CloseItemCoreAsync 是在源文件中实现的,请随意拥有look。在任一分支中使用的ScreenExtensions.TryDeactivateAsync 最终将转发到screen 本身上的DeactivateAsync,该screen 负责清理。

回到您的用例,当您指示从一个项目导航到下一个项目时,可以选择切换回内存中的现有实例,我建议您使用Conductor&lt;IScreen&gt;.Collection.OneActive。然后,您可以查询其Items 集合以查明某个实例是否已经存在,以便激活它或创建一个新实例。

总而言之,激活和停用最好通过导体完成。

如果您需要明确处理,您可以将您的样品更改为以下样品。

protected override void OnDeactivate(bool close)
{
    if (close) 
    {
        Dispose();
    }
}

public void Dispose()
{
    Console.WriteLine("disposed");       
}

但是,在Dispose 中调用base.TryClose(); 是不必要的,并且会导致OnDeactivateTryClose 之间的无限循环。 Dispose 模式仅用于清理非托管资源,例如文件句柄,参考 MSDN


更新

使用 Conductor.Collection.OneActive 不会关闭 ViewModel,但是当我使用 ActivateItem(IoC.Get()); 时,会再次创建 ViewModel,因为我看到它如何再次运行构造函数。我错过了一些东西。

我个人是the pit of success 的坚定支持者,当一个设计良好的框架(例如Caliburn)公开一个静态服务定位器时,我总是觉得有些失望。当我们陷入困境时,我们很容易被诱惑到黑暗的一面。

如前所述:

然后,您可以查询其Items 集合以查明某个实例是否已经存在,以便激活它或创建一个新实例。

要找出某个实例是否已经存在,我们需要一种方法来识别它。它可以基于类型,但为了简单起见,让我们使用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&lt;IHasEntity&gt;.Collection.OneActive,并且不再需要.OfType&lt;IHasEntity&gt;() 过滤器。

【讨论】:

  • 我已经了解到并观察到关于 bool close 的差异。但是,我仍然不明白如何激活已经在集合中的项目(在停用时未关闭)。使用 Conductor&lt;IScreen&gt;.Collection.OneActive 不会关闭 ViewModel,但是当我使用 ActivateItem(IoC.Get&lt;FirstViewModel&gt;()); 时,会再次创建 ViewModel,因为我看到它如何再次运行构造函数。我错过了一些东西。
  • 看看我的更新,你不应该(不需要)在设计良好的应用程序中使用服务定位器模式 (IoC) :)
【解决方案2】:

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会自动创建视图实例...

【讨论】:

  • 谢谢!以及如何使用 ActivateItem 方法实现它?现在,我正在使用ActivateItem(IoC.Get&lt;FirstViewModel&gt;());
  • 你为什么不做ActivateItem(ViewModel)??你能展示你的视图模型吗?
  • 实际上,在 SimpleContainer 中使用 RegisterSingleton (正如您在我的问题的评论中提到的那样)然后使用 ActivateItem(IoC.Get&lt;FirstViewModel&gt;()); 激活就足够了。在我理解 ActivateItem 方法之前,我已经发布了评论。在这个组合中,我认为 HelperConstructor 对简单模式没有任何用处。
  • 如果您的原始问题已解决,请记住接受并投票,如果您有其他问题,请提出新问题:stackoverflow.com/help/someone-answers
猜你喜欢
  • 2020-11-04
  • 2017-12-06
  • 1970-01-01
  • 1970-01-01
  • 2014-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多