Initial screens in conductor

Topics: Bugs, UI Architecture
Apr 2, 2012 at 8:59 AM

Hi,

I'm trying to create a conductor that contain several static screens.

All of the static screens should "run" but only one should display (Active)

I'm expecting that the screen will raise OnInitialize event (method) on start and OnDeactivate(close: true) when closing the conductor.

I've implemented it like this (example):

	public class ShellViewModel : Caliburn.Micro.Conductor<object>.Collection.OneActive
	{
		private ChildViewModel _childViewModel1;
		private ChildViewModel _childViewModel2;
		private ChildViewModel _childViewModel3;

		public ShellViewModel()
		{
			_childViewModel1 = new ChildViewModel {DisplayName = "a"};
			_childViewModel2 = new ChildViewModel {DisplayName = "b"};
			_childViewModel3 = new ChildViewModel {DisplayName = "c"};

			// Maybe like this:
//			Items.AddRange(new object[]
//			               	{
//			               		_childViewModel1,
//												_childViewModel2,
//												_childViewModel3
//			               	});

			ActivateItem(_childViewModel1);
			ActivateItem(_childViewModel2);
			ActivateItem(_childViewModel3);
		}

		protected override void OnInitialize()
		{
			base.OnInitialize();

			// Maybe here: 
//			ActivateItem(_childViewModel1);
//			ActivateItem(_childViewModel2);
//			ActivateItem(_childViewModel3);
		}

		public void Close()
		{
			TryClose();
		}
	}

And the Test ViewModel / Screen (ChildViewModel):

	public class ChildViewModel : Screen
	{
		protected override void OnInitialize()
		{
			base.OnInitialize();

			MessageBox.Show(string.Format("Init: {0}", DisplayName));
		}

		protected override void OnDeactivate(bool close)
		{
			base.OnDeactivate(close);

			if (close)
			{
				MessageBox.Show(string.Format("Closed: {0}", DisplayName));
			}
		}
	}

I've tried to activate or insert the screens to the Items collection at the constructor and at the OnInitialize method.

All those methods didn't work for me: The OnInitialize (on startup) and OnDeactivate (on close) of the screen (ChildViewModel) only executed once for the last screen (DisplayName = "c")

Actual output:

Startup: "Init: c"

Close: "Closed: c"

Expected output:

Startup: "Init: a", "Init: b", "Init: c"

Close:  "Closed: a", "Closed: b", "Closed: c"

 

Thank you.

Apr 3, 2012 at 8:19 AM

Is anyone know a pattern to make conductor with several constant screens?

I didn't found any example for this

Apr 3, 2012 at 10:30 PM
Edited Apr 3, 2012 at 10:30 PM

how are you displaying these child static screens?

Apr 4, 2012 at 9:04 AM
Edited Apr 4, 2012 at 9:10 AM

Hi,

Thank you for reply. I'm doing it by:

1st Scenario:

Changing:

<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding ActiveItem}" />

Displaying:

<ContentControl cal:View.Model="{Binding ActiveItem}" />

 

2nd Scenario: Using docking (Like Visual studio with initial panels - Solution explorer, Properties, etc...)

Apr 4, 2012 at 9:21 AM

I can focus the problem by this example:

		protected override void OnInitialize()
		{
			base.OnInitialize();

			ActivateAll();
		}

		public void ActivateAll()
		{
			ActivateItem(_childViewModel1);
			ActivateItem(_childViewModel2);
			ActivateItem(_childViewModel3);
		}

with the Button:

<Button cal:Message.Attach="ActivateAll" />

On startup: "Init: c"

On click: "Init: a", "Init b"

Why on startup the OnInitialize of Screen "a" and "b" doesn't raise?

Apr 4, 2012 at 9:26 AM

You are using a OneActive conductor, so the screens are activated once they become the ActiveItem (and deactivated once they are not anymore)... moreover, if a screen is already active, it is not activated again, unless deactivated first.

Apr 4, 2012 at 9:31 AM

But I'm activating all the screens one by one on start up (in contractor and/or OnInitialize of the conductor).

Apr 4, 2012 at 9:42 AM
Edited Apr 4, 2012 at 9:42 AM

Unfortunally, you are not! :)

From the Conductor<T>.Collection.OneActive source code:

 

                /// <summary>
                /// Activates the specified item.
                /// </summary>
                /// <param name="item">The item to activate.</param>
                public override void ActivateItem(T item) {
                    if(item != null && item.Equals(ActiveItem)) {
                        if (IsActive) {
                            ScreenExtensions.TryActivate(item);
                            OnActivationProcessed(item, true);
                        }

                        return;
                    }

                    ChangeActiveItem(item, false);
                }

 

From the ConductorBaseWithActiveItem<T> source code:

 

        /// <summary>
        /// Changes the active item.
        /// </summary>
        /// <param name="newItem">The new item to activate.</param>
        /// <param name="closePrevious">Indicates whether or not to close the previous active item.</param>
        protected virtual void ChangeActiveItem(T newItem, bool closePrevious) {
            ScreenExtensions.TryDeactivate(activeItem, closePrevious);

            newItem = EnsureItem(newItem);

            if(IsActive)
                ScreenExtensions.TryActivate(newItem);

            activeItem = newItem;
            NotifyOfPropertyChange("ActiveItem");
            OnActivationProcessed(activeItem, true);
        }

 

The OnInitialized method gets called before conductor activation, so its IsActive property is false and the current ActiveItem is not activated.

Once the conductor is activated, it automatically activates the current ActiveItem, which is the 3rd one, in your example.

Apr 4, 2012 at 9:53 AM
Edited Apr 4, 2012 at 9:55 AM

I do! :)

Caliburn don't let me do it... Why?

Or, how should I do it?

Is it not possible to init all the Items of the conductor on the conductor's start-up? It doesn't make sense...

 

(I knew that this is caliburn's implementation. but why?)

Apr 4, 2012 at 10:46 AM

The Conductor<T>.Collection.OneActive keeps a single active item at time, as the name implies. Using this kind of conductor you cannot activate them through the conductor as you would like to do. Moreover, even if you activated them manually (calling the Activate method on each item during init), the conductor would deactivate the previously activated item as soon as the ActiveItem changes. So, you cannot use the Conductor<T>.Collection.OneActive to achieve what you need.

In other words, in a OneActive conductor activated and selected ar synonyms, while you want them to keep those concepts separated. So, you cannot use it for your purpose.

If you want a conductor that keeps all items as active, and has a selected item too, then you can subclass the Conductor<T>.Collection.AllActive and provide a SelectedItem property to bind to the ComboBox.SelectedItem dependency property.

Apr 4, 2012 at 11:48 AM
Edited Apr 4, 2012 at 11:51 AM

I do need OneActive Conductor since, I want also to manage who is active and the OnActivate and OnDeactivate methods will raise on time.

The problem is with OnInitialize. Not Active / Activate!

So I don't really want to keeps all the items activate.

 

I activate all of the Items only to try make them raise OnInitialize (which doesn't work), not to make all of them activate at once.

 

BTW, Thank you for your replies!

Apr 4, 2012 at 12:04 PM

Iinitialization occurs just before the first activation, at least for every view-model inheriting from Screen

void IActivate.Activate() {
            if (IsActive) {
                return;
            }

            var initialized = false;

            if(!IsInitialized) {
                IsInitialized = initialized = true;
                OnInitialize();
            }

            IsActive = true;
            Log.Info("Activating {0}.", this);
            OnActivate();

            Activated(this, new ActivationEventArgs {
                WasInitialized = initialized
            });
        }

So, when you speak about Initialization, you are indeed speaking about Activation, at least as long as standard CM classes are concerned.

You cannot have a view-model initialized unless you activate it at least once; if you want to use a OneActive conductor, you have to ensure that the ActivateAll code is called upon the conductor first Activation, and not Initialization.

public class ShellViewModel : Caliburn.Micro.Conductor<object>.Collection.OneActive
{
    private ChildViewModel _childViewModel1;
    private ChildViewModel _childViewModel2;
    private ChildViewModel _childViewModel3;

    public ShellViewModel()
    {
        _childViewModel1 = new ChildViewModel { DisplayName = "a" };
        _childViewModel2 = new ChildViewModel { DisplayName = "b" };
        _childViewModel3 = new ChildViewModel { DisplayName = "c" };
    }

    bool _isFirstTime = true;

    protected override void OnActivate()
    {
        base.OnActivate();

        if (_isFirstTime)
        {
            _isFirstTime = false;
            ActivateItem(_childViewModel1);
            ActivateItem(_childViewModel2);
            ActivateItem(_childViewModel3);
        }
    }

    public void Close()
    {
        TryClose();
    }
}

That said, I think that if you really need to perform some initialization in your view-models, and you don't want it to be tied to the activation process, you are better off doing it in their respective constructors, and not in the OnInitialize method, since it is specifically used for activation lifecycle management.

Apr 4, 2012 at 12:15 PM

Thank you! It works!

But I thought that the ONLY different between OnInitialize and OnActivate is that OnInitialize runs in the first activation and OnActivate every activation.

So by this code you make OnActivate works like OnInitialize + solve my problem. Which is kind of patch.

(This solution = OnInitialize + solve my problem = Patch?)

But it still works, so thank you very much! :)

Apr 4, 2012 at 12:20 PM

The real difference is that the IsActive property is false during OnInitialize, while is true on the first OnActivate (the sequence is Initialize -> IsActive = true -> OnActivate). :)

Apr 4, 2012 at 12:30 PM
Edited Apr 4, 2012 at 1:24 PM

Yeah... I know

I think that maybe the design should change to make it works...

Maybe something like when activating the conductor (After the OnInitialize method of the conductor is called) it will initialize all the Items collection.

Here is what I must do know to solve it:

 

		private bool _isFirstTime = true;

		protected override void OnActivate()
		{
			base.OnActivate();

			if (_isFirstTime)
			{
				_isFirstTime = false;
				ActivateItem(_childViewModel1); // 1st
				ActivateItem(_childViewModel2);
				ActivateItem(_childViewModel3);

				ActivateItem(_childViewModel1); // 2nd
			}
		}

 

The reason that I activate child 1 twice: The first time, I want it to be the first item in the Items collection.

The second time (last line) is that I want it to be activated (selected and displayed) when the conductor is first active.

Apr 4, 2012 at 12:50 PM

Note that you could just reverse the activation order, or let the first item be activated last. That said, I would never use the above approach.

When I had to deal with a docking manager, I decided to use a custom AllActive conductor, and not a OneActive, since all screens are indeed active as long as they are displayed in the docking site. They got deactivated(false) when hidden and deactivated(true) when closed (most docking managers differentiate between 'hide' and 'close' concepts). To keep track of the currently selected pane, I provide a SelectedItem property. This way activation and selection are completely detached, as they logically should.

You are free to use the approach you prefer, but I feel that the current CM implementation is indeed correct, taking into account the purpose of the classes involved in this discussion.

On a side note, when you say:

Maybe something like when activating the conductor (After the OnInitialize method of the conductor is called) it will initialize all the Items collection.

You are describing the exact purpose of the AllActive conductor. :)

Apr 4, 2012 at 1:11 PM
Edited Apr 4, 2012 at 1:16 PM

"Note that you could just reverse the activation order, or let the first item be activated last. That said, I would never use the above approach"

As I said, the first activation is to make the first child to be the first in the Items list.

If I would reverse the order the ComboBox will display the items not in the correct order (that can be solved, but not easily) 

So what approach would you use? I'm also looking for a better solution.

"When I had to deal with a docking manager......."

Maybe docking manager doesn't "fit" to OneActive (It's about implementation). But I don't talk only about docking manager, I'm talking about regular view (see Scenario 1).

"You are describing the exact purpose of the AllActive conductor. :)"

No! I still want to manage life-cycle of activation and deactivation of screens, while there is only one screen activated.

This sentence talk about initialization only. Not activation (only the activation of the conductor itself), so, this is true for both OneActive and AllActive conductors.

 

Thank you again :)

Apr 4, 2012 at 1:22 PM

I want it to be look like this (and work)

		protected override void OnInitialize()
		{
			base.OnInitialize();

			// Set static / initial screens
			Items.AddRange(new object[]
			               	{
			               		_childViewModel1,
			               		_childViewModel2,
			               		_childViewModel3
			               	});

			// Activate the first screen
			ActivateItem(_childViewModel1);
		}

Apr 4, 2012 at 1:43 PM

Why can't you move the child OnInitialize code into the relative constructor?

Apr 4, 2012 at 1:50 PM
Edited Apr 4, 2012 at 1:52 PM

It's also the OnDeactivation.

Because the code using the child's context (members/state)

In the OnDeactivation I need to destroy / dispose some child's member

I can so something like that, in the conductor's OnDeactivate:

_childViewModel3.Dispose();

But I don't like this solution since, each child need to handle his own business logic of Deactivation / Activation, its not the conductor's responsibility.

Apr 4, 2012 at 2:17 PM

I really cannot figure out your scenario. From my perspective, if a screen has to perform an action while activated, it is perfectly legit to use the OnInitialize/OnDeactivate(true) to manage the lifecycle of the screen behavior. On the contrary, if the screen has to 'run', as you say, indipendently from the activation logic, I would initialize everything in the constructor. Moreover, if a special action has to take place, I would define a specific interface, and let the conductor call required method when needed, instead of using CM internal methods associated to the screen lifecycle.

Probably, its just a matter of me not understanding your scenario at all, but I still cannot figure out what kind of behavior you want to achieve. I fear that you are entrusting the Conductor with a specialized role, while using some 'standard' functionalities designed to deal with the concept of activation.

If the context (as you call it) should live while the child is active, what is so bad in delaying the initialization until the first activation? If the object is never activated, it should never be initialized... otherwise, if some internal logic of the child requires such intialization, either put the code in the constructor, or consider that the class is not properly designed (maybe the design doesn't take into account the activation/deactivation lifecycle? Or the functionality inherently would require the child to be active?).

Apr 4, 2012 at 2:53 PM
Edited Apr 4, 2012 at 2:53 PM

It's not a design issue, its customer requirements.

The child logic: A timer should start on event (while the conductor is open) and stop when close the conductor, this logic happen while the conductor open

But...

As I'm trying to understand you, maybe I should do something like that:

 

		public ShellViewModel()
		{
			_childViewModel1 = new ChildViewModel {DisplayName = "a"};
			_childViewModel2 = new ChildViewModel {DisplayName = "b"};
			_childViewModel3 = new ChildViewModel {DisplayName = "c"};

			Items.AddRange(new ChildViewModel[]
			               	{
			               		_childViewModel1,
						_childViewModel2,
						_childViewModel3
			               	});
		}

		protected override void OnDeactivate(bool close)
		{
			if (close)
			{
				foreach (var item in Items)
				{
					item.Stop();
				}
			}

			base.OnDeactivate(close);
		}

 

 

The initialization of the children will be in constructor.

This solution is legit.

Apr 4, 2012 at 3:08 PM

I suppose this is a proper solution too. The timer is tied to the Conductor activation (if I read you correctly), and not to the Conducted ones. So, it is better to provide specific logic to handle this case.

Apr 4, 2012 at 3:16 PM

Thank you.

Now I understand much better how CM works :)