简体   繁体   English

在 Caliburn.Micro 中管理 ViewModel

[英]Managing ViewModels in Caliburn.Micro

I am changing an application that I am developing to MVVM pattern, using Caliburn.Micro Framework.我正在使用 Caliburn.Micro 框架将我正在开发的应用程序更改为 MVVM 模式。

As I was getting used to this, at first, I was using the IConductor interface for navigating by inheriting Conductor<object> at the MainViewModel and then navigating the Screens with the ActivateItem method.当我习惯了这一点时,起初,我使用IConductor接口进行导航,方法是在 MainViewModel 继承Conductor<object> ,然后使用 ActivateItem 方法导航屏幕。

I didn't use a container, but instead I was instantiating a new ViewModel every single time.我没有使用容器,而是每次都实例化一个新的 ViewModel。

For example, to navigate to FirstViewModel, I was using ActivateItem(new FirstViewModel());例如,要导航到 FirstViewModel,我使用的是ActivateItem(new FirstViewModel());

The ViewModelels are light on resources so this implementation wasn't noticeable. ViewModelels 对资源很轻,所以这个实现并不明显。 However, I have discovered that the ViewModel instance was not disposed and I have started using Timers to check if the instance is still running, pilling up in the background.但是,我发现 ViewModel 实例没有被释放,我已经开始使用定时器来检查实例是否仍在运行,在后台堆积。

Since then, I am trying all sorts of implementations to control how ViewModels are managed.从那以后,我尝试了各种实现来控制 ViewModel 的管理方式。 What I want is to be able to decide whether I reference an already instantiated ViewModel or instantiate a new one.我想要的是能够决定我是引用一个已经实例化的 ViewModel 还是实例化一个新的。 Also, I want to decide whether I dispose the ViewModel or keep it running in order to reconnect to it later.另外,我想决定是处置 ViewModel 还是让它继续运行以便稍后重新连接。

So, reading the documentation, I have implemented a SimpleContainer in the BootStrapperBase所以,阅读文档,我在 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>();   

        }
    }

I thought that the IoC.Get<FirstViewModel>() would instantiate a new ViewModel or reuse an open one, if it was already instantiated.我认为IoC.Get<FirstViewModel>()将实例化一个新的 ViewModel 或重用一个打开的 ViewModel,如果它已经实例化的话。 However, it is instantiating a new ViewModel every time.但是,它每次都实例化一个新的 ViewModel。

Also, I cannot figure out how to dispose the ViewModel when activating another one.此外,我无法弄清楚在激活另一个 ViewModel 时如何处理 ViewModel。 For example, I have put an OnDeactivate on the FirstViewModel that is triggered when switching to another ViewModel, but I don't know what code should I put there in order to dispose that instance.例如,我在 FirstViewModel 上放置了一个 OnDeactivate,它在切换到另一个 ViewModel 时触发,但我不知道应该放什么代码来处理该实例。 I have tried this Setup, implementing IDisposable interface, but I receive a System.StackOverflowException.我已经尝试过这个安装程序,实现了 IDisposable 接口,但我收到了 System.StackOverflowException。

protected override void OnDeactivate(bool close)
        {

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

Isn't SimpleContainer from Caliburn.Micro enough for managing the ViewModels or should I research on a different approach? Caliburn.Micro 中的 SimpleContainer 还不足以管理 ViewModel,还是我应该研究一种不同的方法?

I know that it seems that I am asking multiple questions but all these questions are regarding a main problem which is about managing the viewmodels.我知道我似乎在问多个问题,但所有这些问题都是关于管理视图模型的主要问题。

Reading the documentation I came across the Lifecycle concept which, I think is the concept that would manage my issues but I didn't find further explanation.阅读文档时,我遇到了Lifecycle概念,我认为这是可以管理我的问题的概念,但我没有找到进一步的解释。

The documentation on Caliburn.Micro doesn't give many examples and I am finding it hard to understand how to use this framework properly without examples. Caliburn.Micro 上的文档没有给出很多示例,我发现如果没有示例,我很难理解如何正确使用这个框架。

You were right to look at IConductor , it's what Caliburn expects us to use to manage component lifecycle.您对IConductor的看法是正确的,这是 Caliburn 期望我们用来管理组件生命周期的。 For completeness, there are also ActivateWith , DeactivateWith and ConductWith extension methods to link Screen lifecycles without the intervention of a Conductor , but I tend to steer away from those.为了完整起见,还有ActivateWithDeactivateWithConductWith扩展方法来链接Screen生命周期而无需Conductor的干预,但我倾向于避开这些。 Though I might use them in an exotic unit test scenario.虽然我可能会在一个奇异的单元测试场景中使用它们。

As mentioned in the documentation, deactivation can have multiple meanings.如文档中所述,停用可以具有多种含义。 Let's use TabControl as an example in combination with an Conductor<IScreen>.Collection.OneActive .让我们将TabControlConductor<IScreen>.Collection.OneActive结合使用作为示例。

  • We could switch from one tab to another.我们可以从一个选项卡切换到另一个选项卡。 We don't want to close the tab we started from, we merely want to deactivate it.我们不想关闭我们开始的选项卡,我们只想停用它。
  • We could close the current tab, switching to (activating) the one at the previous index (Caliburn's default).我们可以关闭当前选项卡,切换到(激活)上一个索引处的选项卡(Caliburn 的默认设置)。

Because of this flexibility, ie the multitude of possibilities, Caliburn doesn't force either behavior onto you.由于这种灵活性,即多种可能性,Caliburn 不会将任何一种行为强加给您。 Of course, this means you have to make the appropriate calls yourself.当然,这意味着您必须自己进行适当的调用。

The first case is easy, assigning a new ActiveItem automatically deactivates the previous one.第一种情况很简单,分配一个新的ActiveItem自动停用前一个。

The second case requires you to close the tab explicitly.第二种情况要求您明确关闭选项卡。 This will, however, trigger Caliburn to assign a new ActiveItem .但是,这将触发 Caliburn 分配一个新的ActiveItem You could use the default strategy, or implement your own, or you could make sure the item is no longer the active one once you close it.您可以使用默认策略,或实施您自己的策略,或者您可以确保该项目在您关闭后不再是活动的。 In that case Caliburn doesn't need to look elsewhere.在这种情况下,Caliburn 不需要寻找其他地方。

The noteworthy extension methods in this context are defined in ScreenExtensions.cs .在此上下文中值得注意的扩展方法在ScreenExtensions.cs中定义。

The simplest method to close an item is await conductor.TryCloseAsync(item) with an optional CancellationToken .关闭项目的最简单方法是await conductor.TryCloseAsync(item)与可选的CancellationToken This method merely forwards to conductor.DeactivateItemAsync(item, true, CancellationToken.None);该方法仅转发给conductor.DeactivateItemAsync(item, true, CancellationToken.None); . .

In case of the Conductor<IScreen>.Collection.OneActive the implementation is given next.Conductor<IScreen>.Collection.OneActive的情况下,接下来给出实现

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

It's all pretty self-explanatory once you know where to look.一旦你知道在哪里看,这一切都是不言自明的。 The close flag is the difference between deactivation and closing the item. close标志是停用和关闭项目之间的区别。 The CloseStrategy is Caliburn's way of enabling graceful shutdown, eg "Are you sure you want to close the item?" CloseStrategy是 Caliburn 启用优雅关闭的方式,例如“您确定要关闭项目吗?” . . CloseItemCoreAsync is implemented next in the source file, feel free to have a look . CloseItemCoreAsync接下来在源文件中实现,随意看看 The ScreenExtensions.TryDeactivateAsync used in either branch will eventually forward to DeactivateAsync on the screen itself, which is responsible for the clean up.在任一分支中使用的ScreenExtensions.TryDeactivateAsync最终将转发到屏幕本身上的DeactivateAsync ,它负责清理。

Back to your use case, as you indicate navigating from one item to the next, with the option of switching back to an existing instance in memory, I advice you to use Conductor<IScreen>.Collection.OneActive .回到您的用例,当您指示从一个项目导航到下一个项目时,可以选择切换回 memory 中的现有实例,我建议您使用Conductor<IScreen>.Collection.OneActive 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.然后,您可以查询它的Items集合以查明某个实例是否已经存在,以便激活它或创建一个新实例。

To sum up, activation and deactivation is best done through the conductors.综上所述,激活和去激活最好通过导体来完成。

If you need explicit disposal, you could alter your sample to the one below.如果您需要明确处置,您可以将您的样品更改为以下样品。

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

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

Calling base.TryClose();调用base.TryClose(); in Dispose , however, is unnecessary and will cause an infinite loop between OnDeactivate and TryClose .但是,在Dispose中是不必要的,并且会导致OnDeactivateTryClose之间的无限循环。 The Dispose pattern is only necessary for cleaning up unmanaged resources, such as file handles, ref MSDN . Dispose模式仅用于清理非托管资源,例如文件句柄,参考MSDN


Update更新

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

Personally I'm a strong proponent of the pit of success , I always find it somewhat disappointing when a well designed framework, such as Caliburn, exposes a static Service Locator.我个人是 成功坑的坚定支持者,当一个设计良好的框架(例如 Caliburn)公开 static 服务定位器时,我总是觉得有些失望。 When we get stuck, we're easily tempted to the dark side.当我们陷入困境时,我们很容易被诱惑到黑暗的一面。

As mentioned:如前所述:

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.然后,您可以查询它的Items集合以查明某个实例是否已经存在,以便激活它或创建一个新实例。

To find out if a certain instance already exists , we need a way to identify it.要确定某个实例是否已经存在,我们需要一种方法来识别它。 It could be based on type, but for the sake of simplicity let's use an int Id property.它可以基于类型,但为了简单起见,让我们使用int Id属性。 Say all (or some) of the view models in the Items collection are decorated with an IHasEntity interface (which exposes the Id prop) and we're looking for Id == 3 .假设Items集合中的所有(或部分)视图模型都装饰有IHasEntity接口(它公开了Id属性),我们正在寻找Id == 3

All you need to do from within the scope of the conductor is something in the lines of:您需要在导体的 scope 内做以下事情:

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

Closing thought, if all your view models implemented the common IHasEntity abstraction, you could define your conductor as Conductor<IHasEntity>.Collection.OneActive and the .OfType<IHasEntity>() filter would no longer be necessary.最后的想法是,如果您的所有视图模型都实现了通用的IHasEntity抽象,您可以将您的导体定义为Conductor<IHasEntity>.Collection.OneActive .Collection.OneActive 并且不再需要.OfType<IHasEntity>()过滤器。

RegisterSingleton in SimpleContainer will do the job... SimpleContainer 中的 RegisterSingleton 将完成这项工作......

So if you want to instantiate at your choice you could use an helper that checks type's constructors with their default parameters: (some knowledge to reflection) after you could adapt the code..因此,如果您想根据您的选择进行实例化,您可以使用一个助手来检查类型的构造函数及其默认参数:(一些反射知识)在您可以调整代码之后..

but if you find that too complex, see first Activator.Createinstance .但如果您觉得这太复杂,请参阅第一个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);
  }
}

you use this helper by giving a Type:你通过给出一个类型来使用这个助手:

var instanceviewModel = HelperConstructor.MyCreateInstance(classType); var instanceviewModel = HelperConstructor.MyCreateInstance(classType);

later caliburn automatically creates the instance of view if needed...如果需要,稍后 caliburn 会自动创建视图实例...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM