Or, How to Determine When Your Add-In Console Tab is Displayed.

Consider this problem. You have some code that runs while your Add-In is displayed in the Windows Home Server Console (a monitoring thread, a timed data refresh, maybe some sort of animation), something that you want to turn off if your Add-In isn’t the currently open Tab.

That’s good coding practice for WHS, I think – be a good neighbour. You don’t want some other developer’s long running task sucking all the CPU cycles while your Add-In is displayed, and they probably feel the same way! The Console has enough stuff loading and running on most Home Servers, so we should really be cleaning up after ourselves as much as possible.

And that means turning our tasks on when our Add-In is displayed, and turning them off when it’s not.

 

The Console Tab Loading Process

So, how can we tell when our Tab is being displayed? Unfortunately, the SDK doesn’t provide us with many clean options. If we trace what happens when a Tab is displayed using Reflector, we find this code:

private void TabButtonClick(object obj, EventArgs ea)
{
    WaitCursor cursor = new WaitCursor(this);
    try
    {
        ...
        TabButton selectedButton = this.selectedButton;
        this.selectedButton = (TabButton) obj;
        this.selectedButton.Prepare();
        this.dataPort.Controls.Clear();
        this.dataPort.Controls.Add(this.selectedButton.TabContentsPanel);
        this.dataPort.Invalidate();
        this.selectedButton.Invalidate();
        ...
        this.selectedButton.RefreshData();
        ...
    }
    catch (Exception exception)
    {
        ...
    }
    cursor.Dispose();
}

We get a WaitCursor while the Tab is loading (an hourglass), and the WHS Console grabs the Tab “button” that just got clicked, calls Prepare() on it, clears the current DataPort (the area of the WHS Console where your Tab displays), and then adds your HomeServerConsoleTab control (the TabContentsPanel of the Tab) to the DataPort. We get some Invalidate() calls to make everything draw, and then the Console calls RefreshData() on your Tab as the last step.

If everything goes well, your control shows up in the DataPort, and your Tab “button” is redrawn to show that it is selected.

We’re all pretty familiar with adding and removing controls (we’ll talk about these events a bit later), but what are these Prepare() and RefreshData() methods? Brendan Grant has a post that outlines what these methods do:

Refresh()

Some add-ins are expected to be able to refresh the displayed data from time to time and because the Home Server Console suppresses any F5 keystrokes making any hooks into KeyDown or KeyPress worthless, an alterative is to use an external mechanism for refreshing such as a timer or a manual refresh button... ITabExtender.Refresh() is our saving grace.

Prepare()

Along with Refresh() which is called just as our tab is being displayed and on the F5 keystroke we also get Prepare() which is executed far less... only when a tab is about to be displayed.

In my experience, Refresh() is not very reliable, as it depends entirely on what has focus when you hit F5. It’s very easy to unfocus the Console while you’re working.

Unfortunately, these methods aren’t part of the basic IConsoleTab interface. To get the Console to run them, your Add-In also needs to inherit ITabExtender, which is undocumented and therefore outside the official SDK. But when has that ever stopped us?

Here’s our event flow so far when someone changes to our Tab:

  1. Prepare() is called on your Add-In
  2. The previous contents of the DataPort are cleared
  3. Your Add-In control is added to the DataPort
  4. Your Add-In control is drawn to the screen
  5. RefreshData() is called on your Add-In

We’ve got a few options to determine when our Add-In Console Tab is displayed, but what about when our Tab is unloaded? Nothing fancy – we need to get knee-deep in parent/child control relationships and control add/remove events. But first, let’s look at doing something when our Add-In is loaded.

 

ITabExtender

In order for our Add-In to inherit the ITabExtender functionality, we need to add a few methods to our HomeServerTabExtender class. Brendan explains what these new methods are used for, but right now we only care about Prepare() and Refresh().

namespace Microsoft.HomeServer.HomeServerConsoleTab.DiskMgt
{
    public class HomeServerTabExtender : IConsoleTab, ITabExtender
    {
        private readonly MainTabUserControl consoleTabControl;

        public HomeServerTabExtender(int width, int height, IConsoleServices consoleServices)
        {
            consoleTabControl = new MainTabUserControl(width, height, consoleServices);
        }

        public void Prepare()
        {
            // Called before tab is shown
            consoleTabControl.ConnectToService();
            
        }

        public Guid SettingsGuid
        {
            get
            {
                return new Guid("MY-GUID-HERE");
            }
        }

        public Control TabControl
        {
            get
            {
                return consoleTabControl;
            }
        }

        public Bitmap TabImage
        {
            get
            {
                return Resources.MyImage;
            }
        }

        public string TabText
        {
            get
            {
                return "My Test Tab";
            }
        }

        public bool GetHelp()
        {
            return false;
        }

        public void Refresh()
        {
            // Called on tab change and F5 in console
        }

        public ITabExtender Next
        {
            get
            {
                return null;
            }

        }

        public ITabStatus Status
        {
            get
            {
                return null;
            }

        }

        public int TabOrdinal
        {
            get
            {
                return 50;
            }

        }
    }
}

We have to add Prepare(), Refresh(), Next, Status, and TabOrdinal. We’re returning a bit of bogus data for the properties we’re not using, but that’s OK – WHS will cope. Be aware of the number you’re putting in TabOrdinal, however, as you’ll be changing the order in which your Tab shows in the Console.

As you can see, I’m calling ConnectToService() method on my control during Prepare(), but I’m not doing anything with Refresh(). The idea is the same though – when you hit F5, the Refresh() method is called and you can do some magic to your control.

When WHS runs through TabButtonClick() for my Add-In, Prepare() is called and the control is instructed to run whatever code is in its ConnectToService() method.

 

Detecting when your Tab is removed

Here’s a thing that tripped me up for a few hours: you can click on your Tab button in the Console repeatedly, even when your Tab is already selected, and that causes the Console to run through the same TabButtonClick() method.

The twist here is that Prepare() is called on your Add-In, then your Add-In control is immediately removed from the DataPort and re-added. It’s the same method that is called when you click on a different tab, so the process works the same – it just happens that your Add-In control is added back to the Console right after it has just been removed.

Why does that matter? Well, I was subscribing to the DataPort ControlRemoved event, and disconnecting from the service when that event was fired for my control. That’s bad in this case as Prepare() was the only thing calling the ConnectToService() code – users would click on the Tab button again, Prepare() would be called, then my control would be removed (triggering the disconnect code), and then added again. Of course, Prepare() wasn’t called after the disconnection, so the Add-In never reconnected to the service, and the user would be presented with an Add-In that had no data.

My solution was to respond to the ControlAdded event instead. This meant I could check to see if my control was the one just added, and if it wasn’t then run the disconnect code. If a user changes to a new tab, I run the disconnect code, and if not I assume that they’re just clicking on my Tab icon again and ignore it.

But how do we get the ControlAdded event for the DataPort? We can’t get a reference to it when the Tab is created, or even when Prepare() is called, because our control hasn’t yet been added to the DataPort. This is where we get a bit messy.

First, we subscribe to our control’s ParentChanged event (you can do this in the Designer for your control, or in its constructor), and handle the event:

private void MainTabUserControl_ParentChanged(object sender, EventArgs e)
{
    if (Parent != null)
    {
        Parent.ControlAdded += Parent_ControlAdded;

        if ((Parent.TopLevelControl != null) && (Parent.TopLevelControl is Form))
        {
            ((Form)Parent.TopLevelControl).Closing += MainTabUserControlParentForm_Closing;
        }
    }
}

I’m doing two things here. The first is that I’m checking to see if my control’s Parent property is not null, and if not, I’m subscribing to the Parent.ControlAdded event. The second is that I’m checking the TopLevelControl to see if it’s a Form (i.e. if it’s the main WHS Console), and subscribing to its Closing event (because I want to do some cleanup if the Console closes).

Now, I’m missing a bit here – I should really keep track of the subscription status somewhere, so I can avoid subscribing multiple times if the user clicks my Tab icon repeatedly. I don’t think this happens very often (I didn’t even know you could do it until I started writing this), so I’m ignoring the issue – it’s not going to hurt anything if I respond multiple times to the same event, and I’m unsubscribing from these events later on anyway.

The ControlAdded event handler looks like this:

private void Parent_ControlAdded(object sender, ControlEventArgs e)
{
    if ((e.Control != this) && (sender is Control))
    {
        Control parent = sender as Control;
        parent.ControlAdded -= Parent_ControlAdded;

        if ((parent.TopLevelControl != null) && (parent.TopLevelControl is Form))
        {
            ((Form)(parent.TopLevelControl)).Closing -= MainTabUserControlParentForm_Closing;
        }

        Disconnect();
    }
}

I’m doing a bunch of casting and checking here, mostly because your code shouldn’t trust data, ever. The general flow is that if the new Control added is not mine, I run the Disconnect() method – at that point, I know that my Tab isn’t the one being displayed, so I should be a good neighbour and turn off all my CPU-hungry polling.

Because I know that I’m now disconnected from my service, I can unsubscribe from the ControlAdded and Form.Closing methods – I don’t care if other controls are added or if the form is closing, because I’ve already performed any cleanup I need to do.

The Form.Closing event handler is essentially the same:

private void MainTabUserControlParentForm_Closing(object sender, CancelEventArgs e)
{
    if (sender is Form)
    {
        ((Form) sender).Closing -= MainTabUserControlParentForm_Closing;
    }

    if (Parent != null)
    {
        Parent.ControlRemoved -= Parent_ControlRemoved;
    }

    Disconnect();
}

Once again, lots of casting and checking, but all I’m doing is unsubscribing from any events I can, and then running my disconnect method.

 

Review

Here’s the basic flow for our Add-In if the user clicks our tab:

  1. Prepare() is called on our Add-In, connecting us to our service and starting the data gathering process
  2. The old Tab is removed
  3. Our Tab is added, and the ParentChanged event handler subscribes us to our Parent’s ControlAdded event and Form.Closing event
  4. Our Tab is drawn on the screen
  5. Refresh() is called on our Add-In

If the user clicks on our Tab again, nothing much happens:

  1. Prepare() is called on our Add-In, and our ConnectToService() method determines that we’re already connected and doesn’t do anything
  2. Our Tab is removed
  3. Our Tab is added, and we ignore the ControlAdded event because the control that was added is us
  4. Our Tab is drawn on the screen
  5. Refresh() is called on our Add-In

When the user changes to a different Tab, we clean up after ourselves:

  1. Prepare() is called on the new Tab
  2. Our Tab is removed from the console
  3. The new Tab is added to the console, and we respond to the ControlAdded event by running our cleanup code if the added control isn’t us, and then unsubscribing from any events we can
  4. The new Tab is drawn on the screen
  5. Refresh() is called on the new Add-In

If the user closes the WHS Console Form, we unsubscribe from any events we can, and then run our Disconnect() method.

 

In closing

Could I have done all of this in the ParentChanged event, and avoided all the complicated subscribing and unsubscribing? Yes, mostly (everything except for the Console closing – that doesn’t fire the ParentChanged event). But where’s the fun in that?

I especially like the Prepare() method, because it lets us run code before our control is displayed. This means we can set everything up beforehand and reduce that ugly redraw time.

Plus, events in .NET are awesome.

posted on Monday, September 28, 2009 6:42 PM | Filed Under [ Windows Home Server Development ]

Comments

No comments posted yet.

Post Comment

Title *
Name *
Email
Url
Comment *  
Remember me
Please add 6 and 8 and type the answer here:

Search

Site Sections

Recent Posts

Archives

Post Categories

WHS Add-In Tutorial

WHS Blogs

WHS Development