2
votes

I am changing an application that I am developing to MVVM pattern, using Caliburn.Micro Framework.

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.

I didn’t use a container, but instead I was instantiating a new ViewModel every single time.

For example, to navigate to FirstViewModel, I was using ActivateItem(new FirstViewModel());

The ViewModelels are light on resources so this implementation wasn’t noticeable. 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.

Since then, I am trying all sorts of implementations to control how ViewModels are managed. What I want is to be able to decide whether I reference an already instantiated ViewModel or instantiate a new one. Also, I want to decide whether I dispose the ViewModel or keep it running in order to reconnect to it later.

So, reading the documentation, I have implemented a SimpleContainer in the BootStrapperBase

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. However, it is instantiating a new ViewModel every time.

Also, I cannot figure out how to dispose the ViewModel when activating another one. 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. I have tried this Setup, implementing IDisposable interface, but I receive a 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?

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.

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.

2
if i have understood you want only one instance of ViewModel? if so why you dont use RegisterSingleton instead of RegisterPerRequest ? Another thing..you speak about the dispose of instance not used...the GC does its job...just one thing to know if you are using EventAggregator with Caliburn dont forget to unsubscribe..that's all... the view and viewmodel linked will be garbage..you could test : just add destructor and set a breakpointFrenchy
just one thing the Garbage collector will do the job only if necessary ...so the time could be long before memory released...after you can always activate the GC with codeFrenchy
Actually, I was using a System.Windows.Forms.Timer to check if the class was destroyed, but that was keeping it aliveCosmin
@Frenchy, I just copy/pasted the bootstrapper configuration without knowing the nuts and bolts of it. So, RegisterPerRequest is always creating a new instance, whereas RegisterSingleton creates only one instance?Cosmin
After your comments, I have reread the documentation regarding SimpleContainer and it is much more understandableCosmin

2 Answers

2
votes

You were right to look at IConductor, it's what Caliburn expects us to use to manage component lifecycle. 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. 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.

  • 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).

Because of this flexibility, ie the multitude of possibilities, Caliburn doesn't force either behavior onto you. 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.

The second case requires you to close the tab explicitly. This will, however, trigger Caliburn to assign a new 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.

The noteworthy extension methods in this context are defined in ScreenExtensions.cs.

The simplest method to close an item is await conductor.TryCloseAsync(item) with an optional CancellationToken. This method merely forwards to conductor.DeactivateItemAsync(item, true, CancellationToken.None);.

In case of the Conductor<IScreen>.Collection.OneActive the implementation is given next.

/// <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. The CloseStrategy is Caliburn's way of enabling graceful shutdown, eg "Are you sure you want to close the item?". CloseItemCoreAsync is implemented next in the source file, feel free to have a look. The ScreenExtensions.TryDeactivateAsync used in either branch will eventually forward to DeactivateAsync on the screen itself, which is responsible for the clean up.

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. 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.

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(); in Dispose, however, is unnecessary and will cause an infinite loop between OnDeactivate and TryClose. The Dispose pattern is only necessary for cleaning up unmanaged resources, such as file handles, ref 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. 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. 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.

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. 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.

All you need to do from within the scope of the conductor is something in the lines of:

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.

0
votes

RegisterSingleton in SimpleContainer will do the job...

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.

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

later caliburn automatically creates the instance of view if needed ...