Creating a DockablePanel-Controlmanager Using C#, Part 7

Creating a DockablePanel-Controlmanager Using C#, Part 7

Pinning and how to hide a docked panel

Welcome to Part 7 of a series of articles on how to create dockable forms. The design is similar to the C# IDE, where you are able to dock the toolbox and various other panels, and where you can see a preview rectangle and some icons while dragging the DockableForm around. These icons and the preview rectangle show you the different ways to drop your panel. To get the whole picture of what I'm doing to create a fully dockable panel, I have included a lot of pictures to demonstrate the entire logic behind the particular topic I'm covering. If this is your first visit to this series, I would really recommend to go through all the previous parts first. Why? Because each of the recent parts include the full source code (up to that point), as well as some retrospective short explanations about the different topics covered already.

As mentioned in earlier lessons, these articles are aimed at programmers who are relatively new to C# (currently reading a C# book, or did some small examples in C#), but also have an existing basic knowledge of programming, (for example, former Visual Basic programmers), and at those who want to learn how to build your own Usercontrols.

The actual article will talk about how to hide the dockable panels and to slide them back to the screen in a fashion similar to the Visual Studio style. I will need to talk about hooking and I'll also cover a short introduction on how to debug a hooked application at a beginner's level.

If you want to follow this article and you haven't read all the other parts yet, you should do this first, or you will not be able to follow these explanations. Here are the links to the previous articles:

Note: Because you have to add some parts here and there, the code becomes more and more complex now and, to be sure where you are, I have added the regions where the code is to find or where it should be created so that it is easier for you to follow. If you still haven't created all the regions by now, you really should do this organizational work first; it will save you hours of work.

In the former articles, you already built the DockableForm with its basic and some more advanced docking actions. Looking to the IDE, you will see that your Toolbox and other panels too are normally pinned, so you can see them all the time or you can use a pin button there to get them temporarily hidden, and only a button on that side of the IDE belonging to this panel shows you where you are able to slide your panel back on the screen, whenever needed.

Your DockingManager already contains some of the things you need for this. Remember that in Part 1, you already added the ToolStrips that you will need for exactly this mechanism. They normally are hidden as long as none of the docked panels are unpinned. Here is a picture of them when they are visible. But, be aware this is only the ToolStrips; the ToolStripButtons for this still need to be designed.

Figure 1: ToolStrips on each side ready to wear ToolStripButtons whenever needed.

And this is how it looks when for example one of two panels, both docked to the left, is unpinned.

Figure 2: Left ToolStripButton shown when a Panel docked to the left side is unpinned.

When designing this mechanism, you have to be aware of the different conditions that may occur, that influence your ToolStripButtons and also your ToolStrip itself, depending on what the user does. Maybe the best way is to have them all in a list:

User's action ToolStrip State
No panel docked ToolStrips invisible
Some Panels docked but all of them pinned ToolStrips invisible
Some panels docked and pinned, at least one unpinned. There are two possibilities: All ToolStrips associated with an unpinned docked Panel are visible, for each unpinned panel one ToolStripButton can be seen on the adequate ToolStrip.
        Mouse outside the area of one of the ToolStripButtons The unpinned panel isn't shown, you only will see all the other pinned panels
Mouse hovers one of the shown ToolStripButtons The unpinned Panel slides on the screen so you can see both the unpinned panel and the corresponding ToolStripButton. This mechanism is at least necessary to re-pin the unpinned Panel.
Mouse hovers one of the unpinned panels that are just visible on the screen caused by the above action The unpinned panel stays on screen as long as the mouse is in the range of the DockingControler where the unpinned panel was added.
You have unpinned a panel that was docked using DockType.Center so it is part of a TabControl In this case, you unpin all the panels contained in this TabControler. On the opposite, you pin all the panels that are part of a specific TabControler, even if only one of them is pinned again by the user.
Hover over one of the ToolStripButtons that is the companion for a panel docked using DockType.Center In this case, you are not only sliding one panel back to be visible, you are taking the whole TabControler with all the panels it contains back to the screen to be visible.
Closing a panel while it was unpinned but visible The ToolStripButton that is associated with this Panel is removed from its ToolStrip. If all ToolStripButtons of a specific ToolStrip are removed, the ToolStrip is hidden again. If the panel was part of a TabControl, it is also removed there. The remaining other unpinned panels stay as they have been before.
Undocking a panel while it is unpinned but visible The ToolStripButton that is associated with this Panel is removed from its ToolStrip. If all ToolStripButtons of a specific ToolStrip are removed, the ToolStrip is hidden again. If the panel was part of a TabControl, it is also removed there. The remaining panels stay as they have been before.
No more unpinned panels exist on a specific DockingControler Don't forget that, in a more complex environment, a ToolStrip could wear Buttons that are accompanied to panels in different DockingControlers. So, it may happen that all ToolStripButtons corresponding to a specific DockingControler are removed. But only if there are no more ToolStripButtons left on a specific ToolStrip it is hidden.

Table 1: User actions and how the program reacts

Creating a DockablePanel-Controlmanager Using C#, Part 7

Figure 3 shows the situation just described in the last point of Table 2. In the given example, you can see two ToolStripButtons on this ToolStrip. You obviously have three DockableForms, where two of them are placed on DockingControler 1 and another one is placed on DockingControler 2. Both DockingControlers are docked to the left so the accompanied ToolStrip for both is the left ToolStrip. On each of the controllers, one panel is unpinned, as you can see the pin Button is switched to the left. You have hovered over both of the ToolStripButtons, so both are visible even if still unpinned. Leaving the space of the left ToolStrip or the area where this DockablePanels are placed will hide them again. Closing or undocking the Panel 'List of Technicians' (the right one) will remove its ToolStripButton from the ToolStrip and remove DockingControler 2, but as there is still another plane unpinned; this doesn't hide the ToolStrip. So, this is a different cycle. Disposing the DockingControler doesn't automatically mean that it will hide a ToolStrip, even if it was logically connected in some way to this DockingControler because it was wearing a single unpinned panel that was going to be removed.

[TwoDockingControlers.JPG]

Figure 3: A more complex situation with unpinned Panels placed on different DockingControlers.

Additionally, as you can see in the design of your ToolStripButtons, you need to be aware if they are placed in a horizontal or vertical direction and if the DockablePanel is using an icon or not.

Unpinning the Panel

Now, begin to get the PinButton working. In the button's Click delegate, you check the actual state of the button. If it's pinned you unpin it; if it's already unpinned, you pin it again. To store the actual state, you use the Boolean field _pinned. In addition, you have to fire an event to that DockingControler where the DockablePanel was just attached.

#region Properties (DockablePanel)
//. . .
void HeaderBox_PinClick(bool pinned) {
   //invert the pinstate 
   _pinned = !_pinned;
   // set the PinButton to the inverted value
   // it changes from pinned to unpinned and vice versa
   HeaderBox.Pinned = _pinned;
   // Preparing for firing Change
   ChangePinButtonEventArgs cb = new ChangePinButtonEventArgs();
   cb.Pinned = _pinned;    // pinned or unpinned
   cb.PanelKey = _key;     // needed to identify which panel's Pin
                           // was clicked
   cb.DockingType = this.DockingType; 
   if (this.ChangePinButton != null) {    // delegate installed?
            ChangePinButton(cb);
   }
}
//. . .
#endregion

The information flow goes from the PinButton on the DockablePanel to the DockingControler. The controler itself is always docked in a basic way; that means it's docked to the left, right, top, or bottom in some way. Figure 3 shows two DockingControlers, both docked to the left. In this case, all unpinned panels would have their ButtonStripButton on the left side's ButtonStrip.

DockingControlers use the information they got from the DockablePanel to immediatly hide the panel, when it is unpinned, and to inform the DockingManager to show the ToolStripButton and the ToolStrip too, if it isn't already shown on the screen. The DockingControler also informs the DockingManager about which ToolStripButton to show and which text should be shown on the ToolStripButton. You need some delegates, events, and enumerations to do that, so you add them in the DockablePanel file:

// just after the namespace declaration in the Dockablepanel.cs
// we do
#region Enumerations & Delegatedeclarations(DockablePanel)
   public enum PanelState {Pinned, UnpinnedOnScreen,
                           UnpinnedHidden};
   internal delegate void
      ChangePinButtonEventDelegate(ChangePinButtonEventArgs e);
#endregion
// and inside the DockablePanel class 
internal sealed partial class DockablePanel : UserControl{
#region events
   internal event ChangePinButtonEventDelegate ChangePinButton;
#endregion
// For getting control about Pinned and unpinned panelstates
private PanelState _state = PanelState.Pinned;

Also, you will need a new argument class to send the message from the Dockablepanel to the DockingControler. You inherit this class from EventArgs and add all the properties you need:

// EventArgsClasses.cs
public class ChangePinButtonEventArgs : EventArgs{
   private bool _pinned;
   private DockType _dockingType;
   private string _panelKey;

   public bool Pinned {
      get { return _pinned; }
      set { _pinned = value; }
   }
   public DockType DockingType {
      get { return _dockingType; }
      set { _dockingType = value; }
   }
   public string PanelKey {
      get { return _panelKey; }
      set { _panelKey = value; }
   }
}

In the DockingControler,you need to add a field to count how many of the DockablePanels added to a specific DockingControler are just unpinned. If you want to unpin one of two panels, both add to the same DockSplitContainer. You must be able to decide whether only one panel must be hidden or if you need to hide the whole DockingControler because both DockablePanels are unpinned. And, you have to add a delegate to get the message from the DockablePanel. You want to create only one instance of this delegate independent of how much panels would be docked or undocked during the lifetime of a DockingControler, so you create one instance of this delegate in the Constructor of the DockingControler class for the whole lifetime of this object.

//DockingControler.cs
//. . . ( we add to the namespaces )
using DockingControls.Classes;

#region Fields (DockingControler)
private int _unpinnedPanels = 0;    // count all unpinned panels
// and the following delegate
private ChangePinButtonEventDelegate _changePinButton;
//. . . 
#endregion

#region Ctor (DockingControler)
internal DockingControler() {
   InitializeComponent();
   //. . .
   _changePinButton =
      new ChangePinButtonEventDelegate(dockPanel_ChangePinButton);
   //. . .
}

In the AddPanel method, you add the delegate to the ChangePinButton event. This, as we already explained in an earlier article, is necessary to get the delegate called when the event is fired.

#region internal Methods (DockingControler)
   internal void AddPanel(DockablePanel dockPanel) {
   //. . . in the last line we add
   dockPanel.ChangePinButton += _changePinButton;
   }
#endregion

This way, the following delegate is working every time someone clicks the PinButton. Depending on the state of the PinButton which is reproduced in e.Pinned, the DockingControler reacts.

#region delegates (DockingControler)
void dockPanel_ChangePinButton(ChangePinButtonEventArgs e) {

   DockablePanel unpinnedPanel =
      _dockingManager.GetDockingPanel(e.PanelKey);
   if (e.Pinned == false) {
      UnpinThePanel(e, unpinnedPanel);
   } else {
      PinThePanel(e.DockingType, unpinnedPanel);
   }
}
#endregion

As done in other articles too, you simply reduced the problem into two different methods. First, PinThePanel() only lets you build a method stub because you will not need that method in the moment. But, you will need to fully implement UnpinThePanel().

#region private Methods (DockingControler)
private void PinThePanel(DockType dockingType,
                         DockablePanel unpinnedPanel) {
    // we will do this a bit later
}
//. . . 
#endregion

Let me explaain some of the following code before you code it. Normally, you will unpin only one DockablePanel when clicking to the PinButton. Only when clicking to the pin of a panel, which is docked in DockType.Center, you change the pinning state of all panels in this DockingControler. Each of the ButtonStripButtons needs to 'know' to which DockablePanel it is connected and to which DockingControler this panel is attached, so when you hover, the Button the message is sent to that very Controler where the panel is situated and panel and controler react in the desired way. Therefore, you need to be able to hand over a list of keys to the ShowButtonStrip() method of the DockingManager. Note that you are using a fixed coupling betwen the DockingManager and the DockingControls by calling a method of the DockingManager. You don't need a delegate here because there is only one DockingManager in your program and every dockingControler is connected to this DockingManager.

Creating a DockablePanel-Controlmanager Using C#, Part 7

If you have only one panel docked to the DockingControler and this panel is unpinned, you call ShowButtonStrip with this data, set your _unpinnedPanels counter to one, and, because the panel is unpinned and you only have this one panel added to the DockingControler, you have to hide the DockingControler by setting it to invisible. If there are more then one panels docked to this DockingControler, you need to take care that the correct panel is hidden. You do this simply by collapsing the corresponding DockSplitContainers panel using the correct PanelCollapsed property. If both panels are unpinned, you have to hide the DockingControler just as you had before. The third possibility is unpinning a panel docked using DockType.Center. This I already have mentioned before. You are looping through the TabControler, switching all the panels from pinned to unpinned.

#region private Methods (DockingControler)
private void UnpinThePanel(ChangePinButtonEventArgs e,
   DockablePanel unpinnedPanel){
   List<string> panelKeys = new List<string>();
   unpinnedPanel.PanelState = PanelState.UnpinnedHidden; 
   if (_dockedPanelsCount == 1){
      panelKeys.Add(unpinnedPanel.Key);
      _dockingManager.ShowButtonStrip(this.Dock, _key, panelKeys);
      _unpinnedPanels = 1;
      this.Visible = false;
   } else {
      // more then one panel is docked
      if (e.DockingType != DockType.Center){
         switch (e.DockingType){
            case DockType.LeftSide:
            case DockType.Upper:
               if (DockSplitContainer.Panel2Collapsed == false){
                  // no one was unpinned before in this controler
                  // so we collapse panel1
                  DockSplitContainer.Panel1Collapsed = true;
                  panelKeys.Add(unpinnedPanel.Key);
                  _unpinnedPanels = 1;
               } else { 
                  // both would be unpinned, so we make the
                  // controler invisible
                  panelKeys.Add(unpinnedPanel.Key);
                  _unpinnedPanels = 2;
                  this.Visible = false;
               }
               break;
            case DockType.RightSide:
            case DockType.Lower:
               if (DockSplitContainer.Panel1Collapsed == false){
                  // no one was unpinned before in this controler
                  // so we collapse panel2
                  DockSplitContainer.Panel2Collapsed = true;
                  panelKeys.Add(unpinnedPanel.Key);
                  _unpinnedPanels = 1;
               }else{
                  panelKeys.Add(unpinnedPanel.Key);
                  _unpinnedPanels = 2;
                  this.Visible = false;
               }
               break;
         }
      }else { 
         // Center
         _unpinnedPanels = 0;
         for (int i = 0; i < _tabControler.TabPages.Count; i++){
            unpinnedPanel =
               (DockablePanel)_tabControler.TabPages[i].Controls[0];
            panelKeys.Add(unpinnedPanel.Key);
            // Synchronize all buttons
            unpinnedPanel.SetPinButton(false);
            unpinnedPanel.PanelState = PanelState.UnpinnedHidden;
            _unpinnedPanels++;
         }
         this.Visible = false;

      }
      _dockingManager.ShowButtonStrip(this.Dock, _key, panelKeys);
   }
}

To set the PanelState of a DockablePanel, you add a property to the DockablePanel class. You may wonder why this is necessary; it's because the DockablePanel wears the PinButton and is the originator of the unpinning action. There is a difference. The _pinned field only stores if the button is pinned or unpinned, which doesn't necessarily really reflect the DockablePanels condition. This, as you see in the enumeration, could be Pinned, UnpinnedOnScreen, and UnpinnedHidden. UnpinnedOnScreen occurs when you hovering the ButtonStripButton. The panel still is unpinned, but will slide onto the screen and stay there as long as the mouse cursor is in the range of the corresponding button or in the range of the DockingControler this DockablePanel is part of.

#region Properties (DockablePanels)
public PanelState PanelState {
   get {
      return _state;
   }
   set {
      _state = value;
   }
}
#endregion

SetPinButton() is needed to set the PinButtons directly into a defined state without clicking to this button. It's used to synchronize the pin buttons of all DockablePanels that are added to a TabControler when DockType.Center is used. Additionally, you need to reset the PinButton of each DockablePanel at startup time, when the DockablePanel is shown onscreen the first time.

#region methods (DockablePanels)
internal void SetPinButton(bool pinned) {
   if (pinned) {
      HeaderBox.Pinned = true;
      _pinned = true;
   } else {
      HeaderBox.Pinned = false;
      _pinned = false;
   }
}
#endregion

This way, you are setting the starting condition of the PinButton when a DockablePanel is loaded the first time to the screen as pinned.

#region Delegates (DockablePanels)
   private void DockablePanel_Load(object sender, EventArgs e) {
      //. . . just in the line before we make the pin invisible
      // we set the PinPnel to pinned
      SetPinButton(true );
      HeaderBox.PinVisible  = false;
   }

Before you can show the ToolStrip, you need to fill it with the new buttons. This is done in the following ShowButtonStrip() method, which is part of the DockingManagers methods. GetButtonStrip() is used to find out which of the four existing ToolStripButtons should be shown. As a side effect, you get the direction the new StripButton needs to be orientated. Looping through the list of panelKeys that needs to have their corresponding button in that ToolStrip, you check whether they already exist and add all of them that aren't already there. As you know, in most cases the list only wears one item; only if you are unpinning a panel that is part of a TabControl (docked in Docktype.Center), you have some more items to add. The IsNewKey() method loops through all items added to a specific toolstrip, checking whether a given panelKey already exists. So, the whole mechanism goes from one panelKey to the next and checks every one of them whether it exists on a specific ToolsStip going through the collection of StripButtons there.

#region internal methods (DockingManager)
internal void ShowButtonStrip(DockStyle dockStyle,
                              string controlerKey,
                              List<string> panelKeys) {
   direction dir;
   ToolStrip buttonStrip = GetButtonStrip(dockStyle, out dir);
   if (buttonStrip != null) {
      for (int i = 0; i < panelKeys.Count; i++) {
         if (IsNewKey(buttonStrip, panelKeys[i])) {
            ToolStripButton stripButton =
               CreateToolStripButton(dir, controlerKey,
                                     panelKeys[i]);
            buttonStrip.Items.Add(stripButton);
         }
      }
      buttonStrip.Visible = true;
   }
}
#endregion

In the DockingManager, you will need some new enumerations and you plan to use some API calls a bit later, so you add the namespace of the WinAPI.

// DockingManager.cs
using DockingControls.WinAPI;

public partial class DockingManager : UserControl {
   // enumerations
   private enum direction {Horizontal, Vertical}
   //. . . 

In GetButtonStrip(), you use the DockStyle the DockingControler is docked to to decide which ToolStrip should be used and which direction the StripButton will be oriented.

#region private methods (DockingManager)
private ToolStrip GetButtonStrip(DockStyle dockStyle,
                                 out direction dir) {
   dir = direction.Horizontal;
   ToolStrip buttonStrip = null;
   switch (dockStyle) {
      case DockStyle.Left:
         dir = direction.Vertical;
         buttonStrip = LeftButtonStrip;
         break;
      case DockStyle.Right:
         dir = direction.Vertical;
         buttonStrip = RightButtonStrip;
         break;
      case DockStyle.Top:
         dir = direction.Horizontal;
         buttonStrip = TopButtonStrip;
         break;
      case DockStyle.Bottom:
         dir = direction.Horizontal;
         buttonStrip = BottomButtonStrip;
         break;
      default:
         buttonStrip = null;
         break;
   }
   return buttonStrip;
}
//. . .
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 7

There is only one additional point to all the explanations already done. To fully understand the following method, you need to know this: To create a relation between a StripButton and a specific DockablePanel, its panelKey is stored in the StripButton's AccessibleName.

#region private methods (DockingManager)
private bool IsNewKey(ToolStrip buttonStrip, string key) {
   for (int i = 0; i < buttonStrip.Items.Count; i++) {
      if (((string)buttonStrip.Items[i].AccessibleName) == key) {
         return false;
      }
   }
   return true;
}
//. . .
#endregion

CreateToolStripButton() creates a ToolStripButton object. The DisplayStyle depends on the fact of whether the DockablePanel has added an image or not. The button also stores that DockablePanel key, which it represents and also stores information about the DockingControler on which this DockablePanel can be found. Therefore, the DockingControler's key is stored in the Tag property of the button and the key of the DockablePanel is stored in the AccessibleName property of the button, as mentioned before.

#region private methods (DockingManager)
private ToolStripButton CreateToolStripButton(direction dir,
                                              string controlerKey,
                                              string panelKey) {
   // extracting the Image of the DockablePanel
   DockablePanel unpinnedPanel = this.GetDockingPanel(panelKey);
   Image panelImage = unpinnedPanel.WindowImage;
   ToolStripButton tsbButton =
      new System.Windows.Forms.ToolStripButton();
   // The display style of a button depends on if there is a
   // picture or not
   if (panelImage != null) {
      tsbButton.DisplayStyle =
         ToolStripItemDisplayStyle.ImageAndText;
      tsbButton.Image = panelImage;
   } else {
      // only text, no picture
      tsbButton.DisplayStyle = ToolStripItemDisplayStyle.Text;
   }
   tsbButton.ImageTransparentColor = System.Drawing.Color.Magenta;
   tsbButton.Size = new System.Drawing.Size(22, 65);
   tsbButton.Text = unpinnedPanel.Caption;
   // The DockingControlers Key is stored in the ,Tag' property 
   tsbButton.Tag = controlerKey;
   // The text is localized
   tsbButton.ToolTipText = Localize.GetResourceString("Txt_Show",
      "DockingControls", "StringResource") + " " +
      unpinnedPanel.Caption;
   // store the DockablePanels key in the 'AccessibleName' property
   tsbButton.AccessibleName = panelKey;
   if (dir == direction.Horizontal) {
      tsbButton.TextDirection =
         System.Windows.Forms.ToolStripTextDirection.Horizontal;
      tsbButton.TextImageRelation =
         System.Windows.Forms.TextImageRelation.ImageBeforeText;
      tsbButton.Name = "dockButtonHor";
   } else {
      tsbButton.TextDirection =
         System.Windows.Forms.ToolStripTextDirection.Vertical90;
      tsbButton.TextImageRelation =
         System.Windows.Forms.TextImageRelation.ImageAboveText;
      tsbButton.Name = "dockButtonVer";
   }
   tsbButton.Visible = true;
   return tsbButton;
}
//. . .
#endregion

For the localization of the control, you add the following item to the StringResource.resx:

Name Value
Txt_Show Show

If you compile at this point, you should be able to unpin panels and to show the StripButtons that represent them. But, you cannot get the panels visible again at the moment.

[AbleToUnpin.JPG]

Figure 4: The result of unpinning Panels.

Another point you have to look for is the need to know the mouse position all the time so that you are informed when you leave the area where an unpinned panel should stay on the screen after you have brought it to be shown by hovering over the related ToolStripButton. How should this be done? When constructing your controls, you don't know how they are used, which Form will be derived from your DockableForm, or which controls are there to be positioned on them. Moving over this DockableForms, as you know, is all the time a fake: What you see on the screen are the DockablePanels, the MDI Form, probably some child forms of the MDI, and lots of controls. Maybe you should capture all these MouseMove events only to find out just where the mouse is? Sorry; this is not a good idea. You need to track the Mouse events independent of where the mouse is. This can easily be done by Hooking.

Hooking the Mouse Events

Here is a definition I found in Wikipedia. I think it couldn't be explained better, so I'll simply repeat this definition here for you: Hooking is a programming technique to make a chain of procedures as an event handler. Thus, after the handled event occurs, control flow follows the chain in a specific order. The new hook registers its own address as handler for the event and is expected to call the original handler at some point, usually at the end. Each hook is required to pass execution to the previous handler, eventually arriving at the default one; otherwise, the chain is broken. Unregistering the hook means setting the original procedure as the event handler. Hooking mouse events means to interfere into the message chain of the mouse events and thus we are able to be informed on all mouse actions which will occur.

[MessageChain.jpg]

Figure 5: An unhooked message chain when a control has subscribed a delegate to the mouse move event messages (simplified).

The address of the delegate is registered to the MouseMove event; therefore, the message stream is sent to this delegate as long as the mouse cursor is inside the visible area of this control on the screen or the control has captured the mouse. You easily can test this in your application. If you drag around a DockablePanel on the screen, the HeaderBox constantly gets the mouse move events, even when you are moving over a docking button and your DockablePanel isn't the top control on the screen; the DockingButtons and also the transparent DockingPreview Form are situated above this control. But, as the HeaderBox has captured the mouse (you still keep the left mouse button down), all the mouse move events are still sent to the mouse move event delegate of the HeaderBox HeaderBox_MouseMove().

Creating a DockablePanel-Controlmanager Using C#, Part 7

When you hook the mouse events, all this still works but before the mouse event messages are sent to that controls where they are designated, they are sent to that delegate that you hook into this chain.

[MessageChainHookedSimplified.JPG]

Figure 6: A hook procedure is inserted into the chain so the messages are going to this procedure before they go to their destination.

There is one simplification in this drawing because you never know who else will also want to hook in this chain. Therefore, this needs a bit more. You also need to allow other implementations to hook in; you can do this by an API method called CallNextHookEx(). So, you need an API call to register to a specific hook; this is done using SetWindowsHookEx(...). Here, you need to declare which type of chain to which you want to hook. You also can hook other chains such as the keyboard, to give an example. You should know that there are other hooks too, as you will learn about later. Also, you need to inform the system which procedure you are using to be called when a message in the hooked chain is sent. So, the full concept looks like this:

[MessageChainHooked.JPG]

Figure 7: This way, you also allow other applications to hook into the mouse messages chain.

The method to get the required information out of this message chain is to check all the messages for that ones you need and the ones you do not need. If you don't need a message because it's, for example, not the type of message you are expecting you also don't want to evaluate it. A method to sort out specific data is called a filter. Talking technically about how this method is implemented depends on which language you are using to describe that process. In C++, methods are passed to other code as an argument so they can be called by generating events are named callback procedures. In C# this is usually a delegate.

So, in talking about these methods by looking to their purpose you can talk about filter procedures. From the viewpoint of how this is arranged, you are talking about the 'hook delegate.'

Hooking could easily lead to major trouble if it is done without knowledge about what's going on and how the messages are working in a Windows operating system. If your hook breaks, the message chains and system messages go out of control; the whole OS may be heavily disturbed. You need to be aware that in using hooking, you are able to intercept other threads, and maybe other applications are also hooking into the same chain that you are. Therefore, the filters you implement need to fulfill minimal standards to grant that all the other filters possibly implemented aren't impeded or blocked by accident. You have to know that, by using hooking, you are able to interfere in a way to remove a message from the chain, and doing so is named 'eating up' the message. If you do so, the message ends in the filter where it is eaten and doesn't ever reach the form or control it was originally designated to.

As a general rule, I would suggest that you never eat up a message if you aren't totally sure what this means for your own application and in which way this may effect other applications too.

There may be different patterns as how to create the filter method that could be considered, to get hooking in a secure way and to allow your or other hooks to be able to forward or to eat up the messages, depending on the needs given by your design.

The system itself handles hooks in the way of a LIFO list (LIFO stands for Last In First Out), which as you know is the standard way a stack works. When I try to explain how LIFO works, I normally compare this with a stack of boards. When you are piling up boards, you put one on top of the other, one by one. After doing this for a while, only people wanting to die would try to draw a board from the bottom of the stack. If you need to get your boards back from the stack for usage, you surely will take the board from the top of the stack because this is the next you will get. And, because this was the last one someone has put onto this stack. If you remember this example, you'll no more forget that a stack works in a LIFO way. As you see in Figure 7, the way you do this exactly fulfills the condition described by the definition at the beginning.

Registering a hook, as already stated, needs to call the API call SetWindowsHookEx(...). This method implements the filter method in adding this delegate on top of the stack so the last added will be the first that will be executed in the message chain. The address for the hook where you have added is returned by this method. CallNextHookEx simply executes the next filter delegate in the list, which is exactly that one that was already there, before you added your hook. Obviously, you need to pass the handle you had returned from SetWindowsHookEx() on to CallNextHookEx() when calling it.

If there are no more delegates to be executed, it returns, and because all these methods are cascaded into each other, you are in fact returning to the point in the chain where the message was hooked by your filters. Here, depending on the value returned by this chain of hooks, you decide whether the message is discarded or if it is sent to the original designated receiver.

[HookDelegate.jpg]

Figure 8: The pattern of a filter delegate in detail

As you see, the next hook is called either if you are not interested in the type of message in the message chain or after you have evaluated the message data and decided to not eat up the message. In this case, it depends on the other filters if the message is eaten by one of them or not.

There is one additional point that has to be explained here. You will need to hook the mouse events to show an unpinned DockablePanel after you have hovered over its ToolStripButton as long as the mouse stays in the range of that DockingControler to which the DockablePanel is added. So, you don't want to do the message evaluation in the filter itself. Instead of that, you simply send the data caught by the hook to that very DockingControler where you need to evaluate them. Here, it will be simple to compare the mouse position's data with the DockingControler area's rectangle to decide if you still will keep the window visible, or if you want to hide it again because the mouse has left DockingControlers space.

The delegate that you will need for your Filter must have the same pattern a C++ CallBack method used in that place normally would have done. This delegate has the following signature:

int HookDelegate(int code, UIntPtr wParam, IntPtr lParam)

The parameters bear the following information:

Name Explanation
wParam Contains the mouse message Identifier
lParam This is a pointer to the MOUSEHOOKSTRUCT structure, a C++ struct you need to reproduce in C#. This will be done by use of marshalling; therefore, you will need to install System.RuntimeInteropServices
code Identifies possible conditions specific to the hook

Table 2: The hook delagates paramteres

In case of hooking, the mouse 'code' can contain only two different values.

Name Value Explanation
HC_ACTION 0 wParam and lParam contain information about mouse messages that have been removed from the message queue.
HC_NOREMOVE 3 wParam and LParam contain information about mouse messages that have been read but not removed from the message queue.

Table 3: Possible values the 'code' parameter may contain.

In case this code contains values less then zero, the general rule in hooking is to instantly call CallNextHookEx() because this would be an invalid hook. After all this theory, you can start to create this class to hook into the mouse message chain. At first, you will add the needed API calls to the APICall class you already created in former articles.

// class APICall namespace DockingControls.WinAPI in Folder WINAPI
[DllImport("user32.dll")]
internal  static extern IntPtr SetWindowsHookEx(HookType code,
   HookDelegate func,
   IntPtr hInstance,
   int threadID);

[DllImport("user32.dll")]
internal static extern int UnhookWindowsHookEx(IntPtr hhook);

[DllImport("user32.dll")]
internal static extern int CallNextHookEx(IntPtr hhook,
   int code, UIntPtr wParam, IntPtr lParam);

[DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
public static extern int GetCurrentThreadId();
le='font-size:10.0pt;font-family:"Courier New"'>EEEE

Now, into the same Folder that contains the above APICall class, add a new code File; name it Enums.cs and add another one called LocalWindowsHook.cs. In Enums.cs, you add:

using System;
namespace DockingControls.WinAPI {
   public enum Msgs {
      WM_MOUSEMOVE         = 0x0200,
      WM_LBUTTONDOWN       = 0x0201,
      WM_LBUTTONUP         = 0x0202,
   }

   public enum HookType : int {
      WH_MOUSE = 7,
   }
}

Creating a DockablePanel-Controlmanager Using C#, Part 7

Because you wanted to suggest that this is a local hook only (global hooks couldn't be done in the same assembly where the control is built), you have named this File LocalWindowsHook.cs. Here, you will add the MouseHook class. In constructing this class, first build its framework and add the needed parts one by one. At first add the regions you are using for a well-structured design and the required fields and delegates. Because you will need to read MOUSEHOOKSTRUCT data by their references, you will need to add the System.Runtime.InteropServices namespace to marshal this data.

//LocalWindowsHook.cs
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Diagnostics;

namespace DockingControls.WinAPI
{
   // Filter procedure delegates 
   internal delegate int HookDelegate(int code, UIntPtr wParam,
                                      IntPtr lParam);
   // Delegate to send data to the DockingControler for evaluation
   internal delegate void MouseHookEventHandler(object sender,
                                                HookEventArgs e);

   // The MouseHook class
   internal class MouseHook {
   #region Fields and delegate definitions
      // Fields
      // A boolean shows if we have hooked successfully
      private bool _isHooked = false;
      // pointer to the hook address
      private IntPtr _hhook = IntPtr.Zero;
      // the field to store the filter delegate
      private HookDelegate _filterHookProcedure = null;
      // the event called whenever we get hooked mousedata in the
      // filter delegate to forward this data to the
      // DockingController
      internal event MouseHookEventHandler EvaluateHook;
      #endregion
   #region  constructor
      internal MouseHook() {
         // in the Ctor we create an instance of the Filter delegate
         _filterHookProcedure = new HookDelegate(this.FilterHookProc);
      }
   #endregion
   #region properties
      // here we will add properties
   #endregion
   #region methods
      // here we will add methods
   #endregion
   }
}

And now, add the pattern of the Filter method. As you can see, it follows exactly the signature of the HookDelegate and is designed exactly as shown in Figure 8. Note that you set code < 0 as a wrong code only. This way, you accept both HC_ACTION and HC_NOREMOVE input; this means you also will use information that was already marked to be removed (eaten). This way, you are independent if other controls try to eat up specific messages.

#region methods
internal int FilterHookProc(int code, UIntPtr wParam,
                            IntPtr lParam) {
   if (code < 0) {
      return APICall.CallNextHookEx(_hhook, code, wParam, lParam);
   }
   // Forward the mousedata to the DockingControler
   HookEventArgs e = new HookEventArgs();
   e.wParam = wParam;
   e.lParam = lParam;
   if (OnEvaluateHook(e)) {
      return 1;
   } else {
      int ret = APICall.CallNextHookEx(_hhook, code, wParam, lParam);
      return ret;
   }
}
#endregion

The Filter method fires EvaluateHook to get all the hooked mousedata delegated to the DockingControler. This follows the standard procedure as it is usually done. Only one point is a bit different. You have to take into account the fact that your HookEventArgs will return a value if this mouse message needs to be consumed (eaten up). Therefore, the method delivers a boolean return value for this.

#region methods
private bool OnEvaluateHook(HookEventArgs e) {
   if (EvaluateHook != null)
      EvaluateHook(this, e);
   return e.consumed;
}

To check whether hooking the mouse messages was successful, you use a boolean property.

#region properties
   internal bool IsHooked {
      get { return _isHooked; }
   }
#endregion

Additionally, you need to create methods to register and unregister the hook. There, you also set the above _isHooked field, which can be accessed by its property. SetWindowsHookEx basically gets informed about which type of hook is used, the address of your filter delegate (C++ people would say callback), and the assembly's threadID.

#region methods
internal bool Register() {
   // we dont register again if we have already registered
   if (!_isHooked) {
      int threadID = APICall.GetCurrentThreadId();
      if (threadID != 0) { // check if we have had success
         _hhook = APICall.SetWindowsHookEx(HookType.WH_MOUSE,
            _filterHookProcedure, IntPtr.Zero, threadID);
         if (_hhook != IntPtr.Zero) {
            _isHooked = true;
         }
      } else {
         _isHooked = false;
         _hhook = IntPtr.Zero;
         throw new Exception("Cannot get threadId");
      }
      if (_hhook != IntPtr.Zero) { _isHooked = true; }
   }
   return _isHooked;
}
#endregion

#region methods
internal void Unregister() {
   if (_isHooked) {
      APICall.UnhookWindowsHookEx(_hhook);
      _hhook = IntPtr.Zero;
      _isHooked = false;
   }
}
#endregion

To get access to MOUSEHOOKSTRUCT, you need to marshal the lparam value you will receive when your filter delegate is called. Add a method to your class to read out the actual mouseposition, which is contained in that structure.

#region methods
internal bool GetMousePosition(IntPtr lparam, ref WinAPI.POINT p) {
   MouseHookStruct mouseStruct =
      (MouseHookStruct)Marshal.PtrToStructure(lparam,
       typeof(MouseHookStruct));
   if (mouseStruct.HWnd == IntPtr.Zero) {
      return false;
   }
   p.x = mouseStruct.Point.x;
   p.y = mouseStruct.Point.y;
   return true;
}
#endregion

To delegate the hooked mousedata to the DockingControler, you need to create a HookEventArgs class that you will derive from EventArgs. You do this in EventArgsClasses.cs.

internal class HookEventArgs : EventArgs {
   private bool _consumed;
   private UIntPtr _wParam;
   private IntPtr _lParam;

   public UIntPtr wParam {
      get {
         return _wParam;
      }
      set {
         _wParam = value;
      }
   }
   public IntPtr lParam {
      get
      {
         return _lParam;
      }
      set
      {
         _lParam = value;
      }
   }
   public bool consumed {
      get {
         return _consumed;
      }
      set {
         _consumed = value;
      }
   }
}

You have designed all the required parts of this class. You now can start running it.

Creating a DockablePanel-Controlmanager Using C#, Part 7

Sliding Unpinned Panels Back on the Screen

To designing that mechanism, you will start where the action begins. It begins when you hover one of the StripButtons. Therefore, you need to create the StripButtons delegate.

The DockingManager is the nerve centre of the whole construction. All panels basically are controlled from this object. If panels are moved around and docked in different places, pinned and unpinned, you will get a lot of actions where a delegate is bound to a Hover event of the different StripButtons. It would be very confusing to permanently create new delegate objects, and it would be absolutely unnecessary to have lots of instances of delegates for this purpose.

Therefore, the design is done using only a single instance of this delegate. To do this, you create a field to create and store the delegate's instance, which will be produced in the constructor.

#region Events and delegate definitions (DockingManager)
   private EventHandler  StripButton_MouseHover;
#endregion

#region Constructors
public DockingManager() {
   InitializeComponent();
   if (DesignMode == true){
     this.Visible = true;
   } else {
     this.Visible = false;
   }
   _admin = new DockAdministration(this);
   // A new instance of the MouseHover Eventhandler is created here
   StripButton_MouseHover =
      new EventHandler(tsbShowPanelButton_MouseHover);
}
//. . .
#endregion

StripButton_MouseHover is a pointer to the instance of the tsbShowPanelButton_MouseHover delegate method. In this delegate, which is called during hovering a StripButton, you retrieve the pointers to the DockablePanel corresponding to the hovered button. This way, you get a pointer to the panel that should be shown and the DockingControler where the panel is placed.

#region delegates (DockingManager)
private void tsbShowPanelButton_MouseHover(object sender,
                                           EventArgs e) {
   if (sender.GetType().Name == "ToolStripButton") {
      ToolStripButton stripButton = (ToolStripButton)sender;
      string panelKey = stripButton.AccessibleName;
      string controlerKey = (string)stripButton.Tag;
      DockingControler dockControl =
         GetDockingControler(controlerKey);
      DockablePanel dockPanel = GetDockingPanel(panelKey);
      dockControl.ShowPanel(dockPanel);
   }
}

The next question to answer is where to add the StripButton's Click delegate to get it working. You want this delegate method to be part of the DockingManager, so the best way is to add the delegate as soon as the StripButton is created and the button is added to the ToolStrip. This, remember, you have just done before in the ShowButtonStrip() method. There, you have:

internal void ShowButtonStrip(DockStyle dockStyle,
   string controlerKey, List<string> panelKeys) {
//. . .
   if (IsNewKey(buttonStrip, panelKeys[i])) {
      ToolStripButton stripButton =
         CreateToolStripButton(dir, controlerKey, panelKeys[i]);
      // here we add the MouseHover delegate of the pin button.
      stripButton.MouseHover += StripButton_MouseHover;
      buttonStrip.Items.Add(stripButton);
   }
//. . .
}

This way, you have found a simple way to select the corresponding DockablePanel that you want to show again. The next step is to design the ShowPanel() method of the DockingControler. The way to show the DockablePanels onscreen depends on how they are docked. In case of basic docking (this is when the DockablePanel seems to be docked to the left, right, top, or bottom), you have only one panel attached to a specific DockingControler. Therefore, in this case you only make the DockingControler visible and are done. If the panel that should be shown again is of a more advanced docking type like Docktype.Upper, DockType.LeftSide, DockType.Lower, or DockType.RightSide, you need to remove the collapsed state of that panel you want to show. Here there are different possibilities. If the DockingControler was still on the screen, only one of the DockablePanels of this DockingControler was hidden by collapsing its DockSplitContainer's panel.

Setting the corresponding panel to PanelXCollapsed = false is always a correct method. (Note that the 'X' in this name is 1 if you are in DockType.Upper or DockType.LeftSide or it is 2 if you are in the DockType.Lower, DockType.RightSide condition.) On the other hand, if both DockablePanels had been unpinned the whole DockingControler was set to invisible. You don't know in which state the DockSplitContainer is because you cannot know the order the panels had been set to unpinned. But, don't worry about that; you simply have to make sure that the other panels still should not be seen on the screen because you have unpinned only one of them. So, you collapse the opposite panel in this case. This all is done in the ShowPanel() method. Still, there is one additional point in each case. You flag all DockablePanels with the PanelState as UnpinnedOnScreen. This is obviously necessary to administrate the different conditions of all the different panels on the screen because the fact of being visible onscreen isn't really significant for them, related to the fact of being pinned or unpinned. A panel could still be unpinned but visible onscreen or unpinned and hidden. On the other hand, it could be pinned and then still be visible onscreen, but then has no corresponding StripButton in any of the ToolStrips.

In the end, you register a MouseHook as explained before, to follow all mouse movements in your entire application and just keep track of where the mouse is onscreen.

#region internal methods (DockingControler)
internal void ShowPanel(DockablePanel dockPanel) {
   DockType dockingType = dockPanel.DockingType;
   dockPanel.PanelState = PanelState.UnpinnedOnScreen;
   string tabName = dockPanel.Key;
   switch (dockingType) {
      case DockType.Left:
      case DockType.Right:
      case DockType.Top:
      case DockType.Bottom:
         this.Visible = true;
         break;
      case DockType.Upper:
      case DockType.LeftSide:
         DockSplitContainer.Panel1Collapsed = false;
         if (this.Visible == false) {
            // it was invisible  means both panels had been hidden
            // means now we should only see panel1 !!
            DockSplitContainer.Panel2Collapsed = true;
            this.Visible = true;
         }
         break;
      case DockType.Lower:
      case DockType.RightSide:
         DockSplitContainer.Panel2Collapsed = false;
         if (this.Visible == false) {
            // it was invisible  means both panels had been hidden
            // means now we should only see panel2 !!
            DockSplitContainer.Panel1Collapsed = true;
            this.Visible = true;
         }
            break;
      case DockType.Center:
         SetUnpinnedPanelsToVisible(this.Key);
         _tabControler.SelectTab(tabName);
         // we remove the type as we add it just after installing
         // the Hook
         this.Visible = true;
         break;
   }
   _mouseHook.Register();
   // we reset the ShutterTimer and all the fields involved in the
   // shutting cycle to make sure it starts new every time
   ResetShutter();
}

There was one thing left in the previous explanations: How do you show a DockablePanel that's docked using Docktype.Center, so it is part of a TabControl? The administration of this is done in the DockAdministration class; therefore, you call SetUnpinnedPanelsToVisible() in the DockAdministration class. In the Admin, you move through all docked panels and check whether they are docked to the DockingControler from which you have called the method. If so, you change the PanelState from UnpinnedHidden to UnpinnedOnScreen. This is the required administrative step. As you know, in a condition of DockType.Center, the full TabControl with all DockablePanels added to it is pinned or unpinned. Therefore, you simply make the DockingControler visible and it's done. Note that the DockControlerKey property is explained and defined a bit later in this article.

private void SetUnpinnedPanelsToVisible(string controlerKey) {
   _dockingManager.Admin.SetUnpinnedPanelsToVisible(controlerKey);
}

#region internal methods (DockAdministration)
internal void SetUnpinnedPanelsToVisible(string controlerKey) {
   foreach (KeyValuePair<string, DockablePanel>
      kvp in DockedPanels) {
      DockablePanel dockPanel = kvp.Value;
      if (dockPanel.DockControlerKey == controlerKey) {
         if (dockPanel.PanelState == PanelState.UnpinnedHidden) {
            dockPanel.PanelState = PanelState.UnpinnedOnScreen;
         }
      }
   }
}
//. . . 
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 7

Automatically Hiding the DockablePanel

If you have slid an unpinned panel back on the screen, it needs to be hidden again when the mouse leaves its range. This means the unpinned panel should slide back into its invisible state. Additionally, you should be aware that maybe a person moves the mouse out of range for a very short period by an unwanted movement. This shouldn't make the panel hidden instantly. Therefore, you use a Timer and instead of really hiding the panel again you only enable this timer, so if you stay outside the DockingControler's area longer than the timer's interval takes to complete, the unpinned DockablePanel will be hidden again. More about how this is done you will see a bit later in the HitTest() method and the shuttingDelay_Tick() delegate. First, you need to have a Timer. So, add a Timer control to the DockingControler and set its properties as shown in the Table 4.

Property Value
Name shuttingDelay
Enabled False
Interval 1000

Table 4: shuttingDelay Timer properties

If you aren't resetting the conditions that define whether the Timer can be enabled, it could happen that the timer would never be enabled; for instance, when the _btnDown value is already set to true by a former action. There are lots of possible actions that can produce undefined states of these values. So, you set them back to their starting conditions immediately before you use them to control the automatic shutting action. You do this at the end of the ShowPanel method because hiding the unpinned panel again needs to be controlled from the very first moment you make the panel visible onscreen.

#region private methods (DockingControler)
private void ResetShutter() {
   _btnDown = false;
   _closePanel = false;
   this.shuttingDelay.Enabled = false;
}

I have talked about hooking a lot in this artile. Now you have seen that you have unpinned the panels, slid them back on the screen; neither need any hooking. So, why have you done this work? I have already mentioned it, but now you need to have a detailed look at the mechanism how the hook is used to automatically hide an unpinned panel, which was shown onscreen by hovering its StripButton. Therefore, in the last lines of the ShowPanel() method, you can see you have used an instance of the MouseHook class and registered a mouse hook. This is the moment in the program where it starts to work when you have implemented it correctly. This implementation has to be done now. Define a private field to keep it for the whole lifespan of the DockingControler and initialize it in the Constructor. Additionally, you add the EvaluateHook delegate where you evaluate the data you got as a result from hooking the mouse messages. Remember, EvaluateHook is fired and gets its data by the FilterHookProc.

//DockingControler.cs
using DockingControls.WinAPI;

namespace DockingControls {
//. . . 

#region Fields (DockingControler)
   private bool _closePanel = false;
   private bool _btnDown = false;

   //. . .
   private MouseHook _mouseHook = null ;
   //. . .
#endregion

#region Constructor
internal DockingControler() {
   InitializeComponent();
   _mouseHook = new MouseHook();
   // at Initialisation we will stay invisible 
   this.Visible = false;
   this.BackColor = SystemColors.ActiveBorder;
   _controlsType = ControlerType.New;
   _prepareClosing =
      new DockedFormPrepareClosingEventDelagate
      (dockPanel_PrepareClosing);
   _undockPanel =
      new DockedFormUndockEventDelegate(dockPanel_Undock);
   _changePinButton =
      new ChangePinButtonEventDelegate(dockPanel_ChangePinButton);
   _mouseHook.EvaluateHook +=
      new MouseHookEventHandler(mouseHook_EvaluateHook);
}
#endregion

In the mouseHook_EvaluateHook delegate, you evaluate lparam that you got as a hook result. Using the GetMousePosition() method of your hooking class, you are able to retrieve the MOUSEHOOKSTRUCT data and, as a result, you get the actual position of the mouse, independent of any control; your just hovering tries to eat up the message. The wParam parameter informs about which message was sent so you can differ among MouseMove, LeftButtonUp, and LeftButtonDown. The check for button up and button down is needed at a later time when you will want to dock a new panel to an unpinned panel, which was just slid back to be visible on the screen, but not fixed.

But don't worry about that now. You only need to understand that when the HitTest fails, the automatic closing procedure is started. This is why you need to hook.

#region Delegates (DockingControler)
private void mouseHook_EvaluateHook(object sender,
                                    HookEventArgs e) {
   WinAPI.POINT pt;
   pt.x = 0;
   pt.y = 0;
   _mouseHook.GetMousePosition(e.lParam, ref pt);
   Msgs wParam = (Msgs)e.wParam;
   switch (wParam) {
      case Msgs.WM_MOUSEMOVE:
         if (!HitTest(pt) && _btnDown != true) {
            // Start Hiding Procedure
            _closePanel = true;
            this.shuttingDelay.Enabled = true;
         } else {
            _closePanel = false;
         }
         break;
      case Msgs.WM_LBUTTONDOWN:
         _btnDown = true;
         break;
      case Msgs.WM_LBUTTONUP:
         _btnDown = false;
         break;
   }
   e.consumed = false;
}
#endregion

The HitTest () method compares the actual mouse position with the combined area of the ToolStrip where the StripButton associated to the DockablePanel is situated and the rectangle of the DockingControler that contains this panel. In Figure 9, I'll show a DockingControler docked using DockStyle.Left as an example how this is calculated. The other DockStyles use the same pattern, calculating the DockingControler's position onscreen, getting the ToolStrip's rectangle, and combining them to a common rectangle that is the summary of both. The ControlsScreenRectangle () and ScreenLocation() methods both use an API call to convert Client coordinates to screen-related values and shouldn't need any further explanation after this amount of explanations in all the articles. There have been lots of similar transformations in the former articles.

[HitRectangle.JPG]

Figure 9: The area where HitTest() succeeds is the summary rectangle of the ToolStrip (green rectangle) and the DockingControler (yellow).

Creating a DockablePanel-Controlmanager Using C#, Part 7

#region private methods (DockingControler)
private bool HitTest(WinAPI.POINT pt) {
   RECT rec = new RECT();
   Rectangle ctlRec;
   switch (this.Dock) {
      case DockStyle.Left:
         ctlRec =
            _dockingManager.ControlsScreenRectangle
            ("LeftButtonStrip");
         rec.left = ctlRec.Left;
         rec.right = ScreenLocation().x + this.Width;
         rec.top = ctlRec.Top;
         rec.bottom = ScreenLocation().y + this.Height;
         break;
      case DockStyle.Right:
         ctlRec =
            _dockingManager.ControlsScreenRectangle
            ("RightButtonStrip");
         rec.left = ScreenLocation().x;
         rec.right = ctlRec.Right;
         rec.top = ctlRec.Top;
         rec.bottom = ScreenLocation().y + this.Height;
         break;
      case DockStyle.Top:
         ctlRec =
            _dockingManager.ControlsScreenRectangle
            ("TopButtonStrip");
         rec.left = ctlRec.Left;
         rec.right = ScreenLocation().x + this.Width;
         rec.top = ctlRec.Top;
         rec.bottom = ScreenLocation().y + this.Height;
         break;
      case DockStyle.Bottom:
         ctlRec =
            _dockingManager.ControlsScreenRectangle
            ("BottomButtonStrip");
         rec.left = ctlRec.Left;
         rec.right = ScreenLocation().x + this.Width;
         rec.top = ScreenLocation().y;
         rec.bottom = ctlRec.Bottom;
         break;
   }
   // Compare if the point is within the rectangle
  if (pt.x > rec.left && pt.x < rec.right && pt.y > rec.top &&
      pt.y < rec.bottom) {
      return true;
   }
   return false;
}
#endregion


#region internal methods (DockingManager)
internal Rectangle ControlsScreenRectangle(string controlName) {
   Control p = null;
   WinAPI.POINT location;
   Rectangle rec = new Rectangle();
   // The location is the 0/0 point of a panel
   location.x = 0;
   location.y = 0;
   switch (controlName) {
      case "LeftButtonStrip":
         p = LeftButtonStrip;
         break;
      case "RightButtonStrip":
         p = RightButtonStrip;
         break;
      case "TopButtonStrip":
         p = TopButtonStrip;
         break;
      case "BottomButtonStrip":
         p = BottomButtonStrip;
         break;
   }
   if (p != null) {
      //Get the location on the screen 
      APICall.ClientToScreen(p.Handle, ref location);
      rec.Location = new Point(location.x, location.y);
      rec.Width = p.Width;
      rec.Height = p.Height;
      return rec;
   }
   return rec;
}
#endregion

#region private methods (DockingControler)
private WinAPI.POINT ScreenLocation() {
   WinAPI.POINT location;
   // The location is the 0/0 point of a panel
   location.x = 0;
   location.y = 0;
   APICall.ClientToScreen(this.Handle, ref location);
   return location;
}
#endregion

In the shuttingDelay_Tick delegate, you hide the unpinned DockablePanel again.

The code deciding to collapse one of the DockSplitContainers panels or to hide the whole DockingControler basically uses the same logic as you already had in the UnpinThePanel() method. There are some small differences such as you have already set the field values for how much panels are unpinned now and the StripButtons are already created and in the correct position onscreen. This needs not be done again. When the last DockablePanel of a specific DockingControler is hidden again so there is no longer any unpinned DockablePanel visible on the screen, the hook is no longer needed. In this case, you unregister the hook. That's the reason you need to know how many DockablePanels of a specific DockingControler are unpinned but visible. If you unregistered the hook too early, you would get into trouble, being unable to hide an unpinned panel again. The _closePanel field is set to false at the end of the method, so the same action cannot be done twice.

private void shuttingDelay_Tick(object sender, EventArgs e) {
   this.shuttingDelay.Enabled = false;
   if (_closePanel) {
      if (UnpinnedPanelsVisibleCount(this.Key) > 0) {
         DockablePanel dockPanel =
            GetNextVisibleUnpinnedPanel(this.Key);
         dockPanel.PanelState = PanelState.UnpinnedHidden;
         switch (dockPanel.DockingType) {
            case DockType.Left:
            case DockType.Right:
            case DockType.Top:
            case DockType.Bottom:
               this.Visible = false;
               break;
            case DockType.Upper:
            case DockType.LeftSide:
               if (DockSplitContainer.Panel2Collapsed) {
                  // Panel2 is colapsed so we need to hide all
                  // panels now
                  this.Visible = false;
               } else {
                  // Panel2 is to be seen so we only hide panel1
                  DockSplitContainer.Panel1Collapsed = true;
               }
               break;
            case DockType.Lower:
            case DockType.RightSide:
               if (DockSplitContainer.Panel1Collapsed) {
                  this.Visible = false;
               } else {
                  // Panel1 is to be seen so we only hide panel2
                  DockSplitContainer.Panel2Collapsed = true;
               }
               break;
            case DockType.Center:
               SetUnpinnedPanelsToHidden(this.Key);
               this.Visible = false;
               break;
         }
      }
      _closePanel = false;
      if (UnpinnedPanelsVisibleCount(this.Key) == 0) {
         // as any panel is hidden again we uninstall the hook
         _mouseHook.Unregister();
      }
   }
}

#region private Methods (DockingControler)
// this is a Wrapper for the real method in the DockAdministration
// class
private int UnpinnedPanelsVisibleCount(string controlerKey) {
   return this._dockingManager.Admin.
   ControlersUnpinnedPanelsVisible(controlerKey);
}

All docked DockablePanels are stored in the DockAdministraion's DpockedPanels Collection. Each panel contains its PanelState because you have stored it as a property of each DockablePanel. If you also know where each of the DockablePanels is added, it should be easy to count all the visible unpinned panels of a specific DockingControler. Therefore, you add a new property to the DockablePanel where you can store the key of the DockingControler to which the DockablePanel was added. You simply add the following property to the DockablePanel class. (The same property is also needed in SetUnpinnedPanelsToVisible() method to make sure to check only panels that are part of a specific DockingControler.)

#region Properties (DockablePanel)
public string DockControlerKey {
   get {
      return _dockControlerKey;
   }
   set {
      _dockControlerKey = value;
   }
}
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 7

Logically, this property is set at the moment you add the DockablePanel to the DockingControler.

#region internal Methods
   internal void AddPanel(DockablePanel dockPanel) {
      dockPanel.DockControlerKey = this.Key;
   //. . .
}
#endregion

Going through the DockedPanels list, you count all of the panels that are part of a specific DockingControler and are unpinned but visible onscreen. This can be seen by their PanelState.

#region internal Methods (DockAdministration)
// is used to find out how much panels on a controler are unpinned
// but visible on screen ( not hidden )
internal int ControlersUnpinnedPanelsVisible(string controlerKey) {
   int count = 0;
   foreach (KeyValuePair<string, DockablePanel> kvp
            in DockedPanels) {
      DockablePanel dockPanel = kvp.Value;
      if (dockPanel.DockControlerKey == controlerKey) {
         if (dockPanel.PanelState == PanelState.UnpinnedOnScreen) {
            count++;
         }
      }
   }
   return count;
}

#region private Methods (DockingControler)
// This is only a Wrapper for the method which is part of the
// Dockadministration
private DockablePanel
   GetNextVisibleUnpinnedPanel(string controlerKey) {
     return this._dockingManager.Admin.
        GetNextVisibleUnpinnedPanel(controlerKey);
}

If more than one panel of a DockingControler is hidden and you leave the area where they are kept visible onscreen, you are closing them one by one independent of which of the buttons or which DockablePanels area were hovered as the last one.

#region internal Methods (DockAdministration)
internal DockablePanel
   GetNextVisibleUnpinnedPanel(string controlerKey) {
   foreach (KeyValuePair<string, DockablePanel> kvp
            in DockedPanels) {
      DockablePanel dockPanel = kvp.Value;
      if (dockPanel.DockControlerKey == controlerKey) {
         if (dockPanel.PanelState == PanelState.UnpinnedOnScreen) {
            return dockPanel;
         }
      }
   }
   return null;
}
#endregion

#region private Methods (DockingControler)
private void SetUnpinnedPanelsToHidden(string controlerKey) {
    this._dockingManager.Admin.SetUnpinnedPanels
       ToHidden(controlerKey);
}
#endregion

If DockablePanels are docked in DockType.Center and they are unpinned but all together visible onscreen, you need to hide them all together, too. In this case, you simply set the PanelState of all DockablePanels to UnpinnedHidden. After calling that method in the shuttingDelay_Tick delegate, you simply need to set the DockingControler to invisible and it's done.

#region internal Methods (DockAdministration)
// Needed for hiding a Tabcontroler with all its docked unpinned
// controls
internal void SetUnpinnedPanelsToHidden(string controlerKey) {
   foreach (KeyValuePair<string, DockablePanel> kvp
            in DockedPanels) {
      DockablePanel dockPanel = kvp.Value;
      if (dockPanel.DockControlerKey == controlerKey) {
         if (dockPanel.PanelState == PanelState.UnpinnedOnScreen) {
            dockPanel.PanelState = PanelState.UnpinnedHidden;
         }
      }
   }
}
#endregion

Pinning the DockablePanel

The method to pin an unpinned panel is really easy to understand, so you will have rather a more superficial view on it. You have two different ways a panel could be docked. DockType.Center uses a TabControler, as you know; all the others work using DockSplitContainer. In DockType.Center, you need to pin all panels that are contained in the TabControler and to remove all the StripButtons associated to them. If the panel that is going to be pinned is docked using one of the other possible DockTypes, you simply remove the appropriate StripButton and pin that panel. Counters for unpinned panels are to be corrected and PanelState needs to be noted to the panels that are pinned. Finally, you test whether this DockingControler still has panels unpinned and visible on the screen. If the answer is no, you don't need the hook anymore, so you unregister it.

That's what you see in the following code.

#region private methods (DockingControler)
private void PinThePanel(DockType dockingType,
                         DockablePanel unpinnedPanel) {
   // more then one panel is docked
   if (dockingType == DockType.Center) {
      // synchronize all Pinbuttons
      for (int i = 0; i < _tabControler.TabPages.Count; i++) {
         DockablePanel pinablePanel =
            (DockablePanel)_tabControler.TabPages[i].Controls[0];
         // all unpinned panels and also that one which was just
         // pinned
         if (pinablePanel.PanelState !=
            PanelState.Pinned || pinablePanel.Key ==
            unpinnedPanel.Key) {
            // only when a panel is unpinned it has a StripButton
            // on the ToolStrip which needs to removed
            // Show PinButton Picture as 'pinned'
            pinablePanel.SetPinButton(true); 
            pinablePanel.PanelState = PanelState.Pinned;
            _dockingManager.RemoveStripButton(this.Dock,
               pinablePanel.Key);
         }
      }
      // no more unpinned panels
      _unpinnedPanels = 0;
   } else {
      // other then DockType.Center 
      _dockingManager.RemoveStripButton(this.Dock,
         unpinnedPanel.Key);
      unpinnedPanel.PanelState = PanelState.Pinned;
      _unpinnedPanels--;
   }
   // in each case we have: 
   if (UnpinnedPanelsVisibleCount(this.Key) == 0) {
      // remove Hook
      _mouseHook.Unregister();
   }
   // panel stays visible on screen now. No more MouseHook messages
   // to this DockingControler
}

The RemoveStripButton() method retrieves the required data about which StripButton is to be removed and on which ToolStrip this Button is placed by usage of the DockablePanels key and the DockStyle the DockingControler is docked.

Because all of your DockingControlers are only being docked left, right, top, or bottom, you know that a left docked panel has its buttons on the left ToolStrip and so on. The GetButtonStrip() method is based on this knowledge. Because you know that the appropriate DockablePanel's key is stored in the AccessibleName of the StripButton, you only need to loop through all of the StripButtons on the correct ToolStrip you just figured out, to look for which StripButton contains the corresponding key. This is done in GetStripButton(). The resulting StripButton is to be removed. If no more StripButtons are part of a ToolStrip, the ToolStrip is hidden. That's all there is to it.

#region internal methods (DockingManager)
internal void RemoveStripButton(DockStyle dockStyle,
                                string panelKey) {
   ToolStripButton stripButton;
   ToolStrip buttonStrip;
   buttonStrip = GetButtonStrip(dockStyle);
   stripButton = GetStripButton(buttonStrip, panelKey);
   if (stripButton != null) {
      // Remove its delegate
      stripButton.MouseHover -= StripButton_MouseHover;
      buttonStrip.Items.Remove(stripButton);
   }
   if (buttonStrip.Items.Count == 0)    // no more buttons
   {
      buttonStrip.Visible = false;
   }
}

private ToolStrip GetButtonStrip(DockStyle dockStyle) {
   ToolStrip buttonStrip = null;
   switch (dockStyle) {
      case DockStyle.Left:
         buttonStrip = LeftButtonStrip;
         break;
      case DockStyle.Right:
         buttonStrip = RightButtonStrip;
         break;
      case DockStyle.Top:
         buttonStrip = TopButtonStrip;
         break;
      case DockStyle.Bottom:
         buttonStrip = BottomButtonStrip;
         break;
      default:
         buttonStrip = null;
         break;
   }
   return buttonStrip;
}

private ToolStripButton GetStripButton(ToolStrip buttonStrip,
                                       string key) {
   if (buttonStrip != null) {
      for (int i = 0; i < buttonStrip.Items.Count; i++) {
         if (((string)buttonStrip.Items[i].AccessibleName) == key) {
            return (ToolStripButton)buttonStrip.Items[i];
         }
      }
   }
   return null;
}

At this point, you are able to compile your code and you will be able to hide and show your DockablePanel. There are still more things you need to do to get this procedure secured against actions done by the user that are not covered in the code you have done in this article. So, for example, users may close panels when they are unpinned but visible or they will undock them by dragging them from one DockingControler to another one. They also may try to dock another panel to a DockingControler that has just slid onto the screen with its DockablePanel still in an unpinned Condition. All this may happen. Therefore, have a look into the dockedPanel_PrepareClosing() delegate and into dockPanel_Undock() to get this handled and workable, too. First, I will discuss the dockedPanel_PrepareClosing() method in the DockingControler.

Creating a DockablePanel-Controlmanager Using C#, Part 7

Closing an Unpinned DockablePanel

What's to be done in that method depends, as you know, on the different DockTypes you have to handle. So, the full method is more or less a big switch statement. Therefore, let me discuss the different situations that may occur and how to handle them. In case of the basic docking actions such as Left, Right, Top, or Bottom, you only have one Dockable Panel added to a specific DockingControler. In this case, when preparing to close a DockablePanel, you need only to remove its corresponding StripButton and you are done. You even don't need to check whether the panel to be closed is an unpinned one because RemoveStripButton() is protected against wrong access and, if no button is found, it doesn't change anything. On the other hand, if a button is found, it will check the amount of remaining buttons on a Toolstrip and hide it, when the last one was removed.

#region delegates (DockingControler)
private void dockPanel_PrepareClosing(DockablePanel dockPanel) {
   DockablePanel remaining;
   switch (dockPanel.DockingType) {
      case DockType.Left:
      case DockType.Right:
      case DockType.Top:
      case DockType.Bottom:
         _dockedPanelsCount = 0;
         _dockingManager.RemoveStripButton(this.Dock,
                                           dockPanel.Key);
         break;
//..

In the Upper and LeftSide cases, but also in the case of Lower and RightSide, you have to add code to remove a possibly existing StripButton, but in addition, you have to check the remaining panels' PanelState. If this is UnpinnedHidden, this would mean you have only the remaining panel added to the DockingControler but, because this is a hidden one, you also need to hide the DockingControler. Note that it is very important in this case that you differ between UnpinnedHidden and UnpinnedOnScreen, because the remaining panel could possibly be unpinned, but the DockingControler only needs to be hidden, when the panel itself is in an UnpinnedHidden condition. Here it is done in code:

case DockType.Upper:
case DockType.LeftSide:
   //. . . and after the following line we add
   remaining.DockingType = GetDockType(this.Dock);
   // Remove a corresponding StripButton if there was one
   _dockingManager.RemoveStripButton(this.Dock, dockPanel.Key);
   // Check if the remaining panel is visible on the screen
   if (remaining.PanelState == PanelState.UnpinnedHidden) {
      this.Visible = false;
   }
   // Correct the counter
   _dockedPanelsCount = 1;
   break;
case DockType.Lower:
case DockType.RightSide:
   //. . . and also here after the following line we add
   remaining.DockingType = GetDockType(this.Dock);
   _dockingManager.RemoveStripButton(this.Dock, dockPanel.Key);
   if (remaining.PanelState == PanelState.UnpinnedHidden) {
      this.Visible = false;
   }
   _dockedPanelsCount = 1;
   break;

In DockType.Center, the whole procedure is done in a method called RemovePanelFromTabControler(), so you have to implement RemoveStripButtons() in this method too. You do this in the loop where you search for the correct panel to be removed. When the panel is found, you remove the associated StripButton.

private void RemovePanelFromTabControler(DockablePanel dockPanel) {
   for (int i = 0; i < _tabControler.TabPages.Count; i++) {
      if (_tabControler.TabPages[i].Controls.Contains(dockPanel)) {
         TabPage tabToRemove = _tabControler.TabPages[i];
         _tabControler.TabPages.Remove(tabToRemove);
         _dockingManager.RemoveStripButton(this.Dock, dockPanel.Key);
   //. . .
   }
//. . .

After all the different cases are handled, you have to do some more general actions such as to remove all delegates that had been added to the DockablePanel and should be closed now. This is already done for different delegates but there are two more that must be removed. You have added the _changePinButton delegate already, but there is one more I havn't spoken about at the moment. Remember the problem of unwanted side effects in the last article? I spoke about the caption there and how to arrange a caption change at runtime for your DockableForm. This again will lead you into trouble regarding the StripButtons text. As you have seen, you used the caption of your DockableForm as the title of your StripButton. This way, the user is informed which StripButton belongs to which panel. Changing the caption of a DockableForm at runtime changes the HeaderText of the DockablePanel that you will see onscreen instead of the DockableForm itself, but the change isn't forwarded to the StripButton at that moment. Therefore, you will additionally have to handle this problem too. But, you do it a bit later. At this moment, you only have to know that there is an additional delegate needed. So, when extending the RemoveDelegate method, you could implement the code for both delegates there.

//In DockingControler.cs we first add the Field to store
//a pointer to the delegate
internal partial class DockingControler : UserControl {
#region Fields
   //. . . just after _changePinButton we add
   private CaptionChangedDelegate _changeCaption;
   //. . .

And now, you extend RemoveDelegates to its full code:

#region private methods (DockingCotroler)
private void RemoveDelegates(DockablePanel dockPanel) {
   if (dockPanel.DelegateStatus("ClosingWhileDocked")) {
       dockPanel.PrepareClosing -= _prepareClosing;
   }
   if (dockPanel.DelegateStatus("Undock")) {
       dockPanel.Undock -= _undockPanel;
   }
   if (dockPanel.DelegateStatus("ChangePicButton")) {
       dockPanel.ChangePinButton -= _changePinButton;
   }
   if (dockPanel.DelegateStatus("ChangeCaption")) {
       dockPanel.CaptionChanged -= _changeCaption;
   }
}
//. . .
#endregion

This, in consequence, requires you to extend the DelegateStatus method in the DockablePanel class. Also, you need to define the CaptionChangedDelegate and an event to fire when changes of the caption occur. All these delegates are checked for existance in the following method. Therefore, the full code of the DelegateStatus() method looks this way.

#region internal methods (DockablePanels)
internal bool DelegateStatus(string type){
   switch (type){
      case "ClosingWhileDocked":
         if ( this.PrepareClosing != null){
            return true;
         }
         break;
      case "PanelClosing":
         if (this.PanelClosing != null){
            return true;
         }
         break;
      case "Undock":
         if (this.Undock != null){
            return true;
         }
         break;
      case "ChangePicButton":
         if (this.ChangePinButton  != null){
            return true;
         }
         break;
      case "ChangeCaption":
         if (this.CaptionChanged != null) {
            return true;
         }
         break;
      default:
         return false;
   }
   return false;
}
//. . . 
#endregion

And here are the definitions you just have seen in the preceding explanation.

#region Enumerations & Delegatedeclarations (DockablePanel)
internal delegate void CaptionChangedDelegate(DockablePanel sender,
   string newCaption);

   internal  sealed partial class DockablePanel : UserControl {
   #region events
      //. . .to the events we add
      internal event CaptionChangedDelegate CaptionChanged;
         //. . .

But, back to DockPanel_PrepareClosing(). Okay, the RemoveDelegates() method is extended now to your needs. But, you still have some common actions to do. You need to count the unpinned but visible panels onscreen related to the DockingControler where PrepareClosing is called. If there are no more of them available, you will unregister the Hook. This is independent of whether there are no more unpinned panels visible. If they all are pinned already, you don't need the hook anymore; if they are hidden, a new hook is registered when you hover over the corresponding StripButton so you also don't need the existing one. Therefore, add:

#region delegates (DockingControler)
private void dockPanel_PrepareClosing(DockablePanel dockPanel) {
//. . .
// Remove all delegates connected to the DockablePanel
RemoveDelegates(dockPanel);
// we correct the amount of unpinned panels of this DockingControler
// if there is nomore panel UnpinnedOnScreen remove Hook
if (UnpinnedPanelsVisibleCount(this.Key) == 0)  {
   _mouseHook.Unregister();
}
// here follows what has been here already before
if (_dockedPanelsCount == 0) {
//. . .no more changes until this method is finished

Creating a DockablePanel-Controlmanager Using C#, Part 7

Now, compile the application. You will see you are able to do all you have already done before. You are able to unpin a panel so it is hidden and to get it back onscreen simply by hovering the mouse over the StripButton. Now, try the new added functionality to remove a panel from the screen by closing it. This should work. Do it again with another panel, during which time it is unpinned but visible onscreen now. And, oh dear; it's terrible, you will see you are crashing.

[ClosingUnpinnedOnScreentwoPanes.JPG]

Figure 10: Closing the DockablePanel in a state where it is unpinned but visible on the screen.

[TheErrorToDebug.JPG]

Figure 11: Closing an unpinned Panel which is visible on the screen still leads to a crash.

But on the other hand, if you are starting to close the panel you get the MessageBox on the screen, and then wait until the panel slides back to be hidden again and then allow it to close, all seems to work properly. (To get this done after clicking the closing button on the panel, you need to move the Mouse to a position where it is out of the panel's range so the panel is able to close.) You will see the hooking action is working even when your request to close the panel is not yet answered. The callback from hooking the mouse messages invokes your delegate to hide the panel even when the panel's closing procedure is waiting for a response. You will have to understand that the mouseHook_EvaluateHook() delegate obviously is called in another thread.

[ClosingHiddenPanel.JPG]

Figure 12: Closing an unpinned panel, which is already hidden. (Hint: Use only one panel for this example.)

This is very valuable to know: If you are using a hook in your application, you need to understand and to take into consideration the fact that you will have different threads running. But, what's going on there? Why does a bug occur? Seems to me, you will have to do some debugging.

How to Debug an Application

People very new to programming are horrified when a crash occurs and they don't understand the reason. You have done such a nice design and now an unexpected error occurs.

And, you are expecting different threads! This is bad, really very bad. The very first thing you have to learn in debugging an application is: Calm down. Debugging is the daily bread of a professional programmer. The second very successful action is: Read and understand the error message. It's sometimes half of the work. Because I live in Austria, my IDE works in the German language, so I need to translate the screenshot, but I'm sure you will get the same message in your language onscreen. The message tells you that an ObjectDisposedException wasn't handled properly. In the hint section, it tells you that you have to make sure that no resource is set to be disposed as long as it is used.

The third action I would consider doing is: Find a way to reproduce the error. Sometimes, errors occur and you have done a lot of actions. Maybe, you aren't really sure what caused the error, especially when the error comes totally unexpected after a lot of steps were done during testing the application you may need to get the problem reduced to a situation that could be reproduced easily. So, you will have Step four: Find the simplest case where a specific error occurs.

What does this mean? In Figure 10, you can see you have two DockablePanels onscreen and tried to close the left one when the error occurred. Two Dockable Panels, both unpinned, both on different DockingControlers. Each DockingControler is hooked; therefore, multiple cases could occur. At this moment, you really don't know the source of this error. So, try to find an easier arrangement where the same error occurs.

[ClosingUnpinnedPanel.JPG]

Figure 13: The same error will be thrown if you are using only one panel.

You will find out that exactly the same error message in the very same line of code will occur if you are only using one panel. So, you see, Step three is very necessary. Different things could happen during searching for other possibilities to produce the same error.

  1. Maybe you will find some more bugs, too
  2. You will find out the error seems to be bound to a very specific case
  3. You will find a very basic configuration where the same error is thrown

In your case, you will find some other bugs if you try to undock an unpinned panel because this isn't coded in the moment. But, the given error could also be verified when you are only using one unpinned panel onscreen and you try to close it. This seems to be the simplest condition where this error is thrown.

Now, you need to decide which actions have to be taken to find out the exact reason for this bug. This is Step five. You analyze the code to find the 'Real Why.' First, have a look at the Call Stack.

[UsingTheCallingStack.JPG]

Figure 14: Using the Call Stack to find out where the bug occurs the first time.

You can see the error is thrown when ScreenLocation() calls the API method ClientToScreen(). Now, you step through this list by clicking one item after the other. Look at the following screenshots and verify this on your own PC.

[UsingCalls_HitTest.JPG]

Figure 15: Going to the previous step in the list, you see that the ScreenLocation() method was called from within HitTest().

Creating a DockablePanel-Controlmanager Using C#, Part 7

[UsingCalls_MoudeHook.JPG]

Figure 16: HitTest() was obviously called from within the MouseHook_EvaluateHook() delegate.

You use this step-by-step method because you could never be sure which method has called a specific other method. One and the same method could be called from lots of other methods where they are implemented. Therefore, the correct way to find out the calling chain is to use this list step by step.

[UsingCalls_FilterHookProc.JPG]

Figure 17: The chain of calls was initiated by the FilterHookProc() delegate.

In Figure 17, you can see the code for original step in the list you just have chosen, highlighted in green, and where it is coming from; this is indicated with a light gray background color.

In the end of this short tracing, you know that a callback done from the mouse hook hits to a DockingControler that is disposed already, because the error message is of type ObjectDisposedException. This is strange because when you prepared for closing, the hook was supposed to be unregistered before you disposed the DockingControler.

private void dockPanel_PrepareClosing(DockablePanel dockPanel) {
//. . .

   // if there is't any panel UnpinnedOnScreen remove Hook
   if (UnpinnedPanelsVisibleCount(this.Key) == 0) {
      // here we unregister the MouseHook !! already
      _mouseHook.Unregister();
   }
   if (_dockedPanelsCount == 0) {
      this.Visible = false;
      // this controler no longer holds panels so it is 
      //going to be disposed and removed from the collection
      this._dockingManager.Admin.RemoveDockControler(this._key);
      // here we are disposing the DockingContoler !!
      this.Dispose();
   }

Now, you need to find out why this could happen and what methods are adequate to find it out.

So Step six is: Formulate a correct question that defines the real problem and should be answered by debugging. This question in your case is: Why is the hook still working after it should have been unregistered? This is the key question you need to answer to find the very basic cause of your problem.

Then I would call Step seven: Find adequate methods to collect the needed information to isolate the hidden reason of the bug, which I have called the 'Real Why.'

Finding this will open he door for a correct handling. In the given situation, setting breakpoints and looking at what's going on will not be successful. Surely, you can use a try-catch block around the full code in the HitTest() method. And, you will do this after you have found the bug, as an additional step to prevent crashes. Doing this in the moment where you are still debugging would only mask the bug, so in the moment this would not be very helpful.

You need a way to follow the steps your code is taking and of being informed about what is done. So, you want to trace the code and get information about important functions. For simplicity, I have decided to use the Console to trace and add the needed code to be informed why the hook is still working after it should have been unregistered.

You will trace what's going on in the hook class: when is the hook registered, and when unregistered again? Only looking for Unregister isn't enough because you could not be sure whether there is anything coming across that might re-register the hook. Do the following now:

  1. Add Console.WriteLine(...)B into the Unregister method.
  2. #region methods (LocalWindowsHook.cs)
    internal void Unregister() {
       if (_isHooked) {
          APICall.UnhookWindowsHookEx(_hhook);
          Console.WriteLine("Unhooked"); 
          _hhook = IntPtr.Zero;
          _isHooked = false;
       }
    }
    
  3. Add Console.WriteLine(...)B into the Register method.
  4. #region methods (LocalWindowsHook.cs)
    internal bool Register() {
       if (!_isHooked) {
          int threadID = APICall.GetCurrentThreadId();
          if (threadID != 0) {    // check if we have had success
             _hhook = APICall.SetWindowsHookEx(HookType.WH_MOUSE,
                _filterHookProcedure, IntPtr.Zero, threadID);
             if (_hhook != IntPtr.Zero) {
                 _isHooked = true;
             }
          } else {
             _isHooked = false;
             _hhook = IntPtr.Zero;
             throw new Exception("Cannot get threadId");
          }
          if (_hhook != IntPtr.Zero) { 
    _isHooked = true; 
             Console.WriteLine("Is Hooked");
          }
       }
       return _isHooked;
    }
    
  5. In dockPanel_PrepareClosing, you also need to add two lines for tracing.

    #region delegates (DockingControler)
    private void dockPanel_PrepareClosing(DockablePanel dockPanel) {
    //. . .
       if (UnpinnedPanelsVisibleCount(this.Key) == 0) {
          Console.WriteLine("Attempt to unhook. Controler key "
                            + this.Key);
          _mouseHook.Unregister();
       }
       if (_dockedPanelsCount == 0) {
          this.Visible = false;
          // this controler no longer holds panels so it is 
          // going to be disposed and removed from the collection
          this._dockingManager.Admin.RemoveDockControler(this._key);
          Console.WriteLine
             ("PrepareClosing is disposing. Controler key "
              + this.Key );
          this.Dispose();
       }
    

    Here, you want to be informed that the delegate really attempts to unregister, and see the moment where disposing is induced.

  6. To checking which DockingControler registers a hook, you add tracing to the ShowPanel() method.
  7. #region internal methods (DockingControler)
    internal void ShowPanel(DockablePanel dockPanel) {
    //. . .
    // just before we register we add
       Console.WriteLine("Attempt tp register.ShowPanel key " +
                         this.Key);
       _mouseHook.Register();
       ResetShutter();
    }
    

Now, you start a debug session and look at what happens when you close a DockablePanel just as you did before. The output you will get is something like Figure 18 shows.

[OutputTracingApp1.JPG]

Figure 18: The Console Output shows the tracings you have set.

Creating a DockablePanel-Controlmanager Using C#, Part 7

This gives you a big step forward to solving the problem. As you can see, the hook is registered but it isn't unregistered again. In truth, you even don't attempt to unregister it. So, the code to attempt unregistering is obviously never executed. Now, you have to examine a much smaller problem: Why doesn't the method UnpinnedPanelsVisibleCount() result in zero as it should? You add an additional trace in the DockAdministration class.

#region internal methods (DockAdministration )
internal int ControlersUnpinnedPanelsVisible(string controlerKey) {
   int count = 0;
   foreach (KeyValuePair<string, DockablePanel> kvp
            in DockedPanels) {
      DockablePanel dockPanel = kvp.Value;
      if (dockPanel.DockControlerKey == controlerKey) {
         if (dockPanel.PanelState == PanelState.UnpinnedOnScreen) {
            count++;
         }
      }
   }
   Console.WriteLine("UnpinnedVisiblePanelsCount " +
                     count.ToString());
   return count;
}

[OutputTracingApp2.JPG]

Figure 19: There is still one panel that counts as unpinned and visible onscreen.

This gives you the 'Real Why.' Very simple. You are counting all panels of this DockingControler whose PanelState is UnpinnedOnScreen. The DockablePanel in the DockPanels collection is deleted after you finished dockPanel_PrepareClosing(). It's still in the Collection as UnpinnedOnScreen and counts this way. This also explains why you don't have trouble when the panel slips back to hidden while you are waiting for the user's input if he really wants to close the panel. A panel when being hidden again unregisters its hook. No problem occurs.

The solution is to correct the state of a DockablePanel before you close it. This is obviously a necessary step to be done in dockPanel _PrepareClosing() to get the correct results. This delegate is called only when the user's decision was to really close the panel, therefore changing the panel's state to pinned just before you close it is the best solution. Adding this will solve the 'Real Why' and the whole bug in a proper way. In the first lines, you add:

#region delegates (DockingControler)
private void dockPanel_PrepareClosing(DockablePanel dockPanel) {
   DockablePanel remaining;
   // we set the panelstate to pinned before we close it,
   // this way it dosn't count for UnpinnedOnScreen any more
   dockPanel.PanelState = PanelState.Pinned;
   //. . .

[OutputTracingApp3.JPG]

Figure 20: The correct result. Hooking works and is unregistered in a correct way.

As mentioned before, you additionally can shape the HitTest() method into a more secure design. So, add a try-catch block there:

#region private methods (DockingControler)
private bool HitTest(WinAPI.POINT pt) {
   RECT rec = new RECT();
   Rectangle ctlRec;
   try {
      switch (this.Dock) {
      // . . .  All the switch cases
      }
      // Compare if the point is within the rectangle
      if (pt.x > rec.left && pt.x < rec.right && pt.y > rec.top
          && pt.y < rec.bottom) {
         return true;
      }
   } catch {
      return false;
   }
   return false;
}

In case of an error, the method performs the catch block and the result is set to false. This immediately causes the code to start the shuttingDelay Timer. The panel is hidden and the hook is unregistered. Maybe there are some calls to this delegate before the hook is unregistered. But, in between, the DockablePanel is removed from the DockedPanels collection. Therefore, the shuttingDelay_Tick() method will only unregister the hook and do nothing else. To test, you simply comment-out (disable) the line you have just added, and the bug should be there again. But now, it should be handled by the try-catch block you have done.

//dockPanel.PanelState = PanelState.Pinned;

Doing a Debug session for testing this part of code, you get the following result:

[OutputTracingApp4.JPG]

Figure 21: Debug Trace with an unhandled bug, but caught by the try-catch block.

Here, you see, it's going exactly as you prognosticated it. The UnpinnedPanelsVisible counts 1 because the bug isn't handled yet. Then, you get three ObjectDisposedException—all of them caught in the HitTest(). In between, as stated before, the DockablePanels are removed from the collection. Therefore, when shuttingDelay_Tick() checks for it, the result is zero and the hook is unregistered. Now, you have tested the try-catch handling so we reinstall the correct handling of the bug by removing the slashes before your code, thus reactivating this line of code like before this little test.

#region delegates (DockingControler)
private void dockPanel_PrepareClosing(DockablePanel dockPanel) {
   DockablePanel remaining;
   // we set the panelstate to pinned before we close it,
   // this way it dosn't count for UnpinnedOnScreen any more
   dockPanel.PanelState = PanelState.Pinned;
      //. . . 

After this short side trip about how to debug this application, I will go on to get your code completed.

Creating a DockablePanel-Controlmanager Using C#, Part 7

Undocking an Unpinned DockablePanel

Undocking the panel is very similar to PrepareClosing because in both cases the panel is removed from the DockingControler. The main difference regarding pinning is that if you undock, you are not only setting the PanelState to PanelState.Pinned as discussed before, but you also have to set the PinButton itself back to pinned. Normally, because most of the code is identical, in such cases you would be able to create a new method that is used in both delegates. But, there are differences that will be seen in the next article where I talk about how to control the focus. Therefore, you need to have two very similar methods here. Because you already have studied DockPanel_PrepareClosing very extensively, I'll show only the full code for the DockPanel_Undock() delegate.

#region delegates (DockingControler)
private void dockPanel_Undock(DockablePanel dockPanel){
   DockablePanel remaining= null;
   this._dockingManager.Admin.RemoveDockedPanel(dockPanel.Key);
   dockPanel.Visible = false;
   dockPanel.Dock = DockStyle.None;
   switch( dockPanel.DockingType){
      case DockType.Left:
      case DockType.Right:
      case DockType.Top:
      case DockType.Bottom:
         if (this.DockSplitContainer.Panel1.Controls.
            Contains(dockPanel)){
            this.DockSplitContainer.Panel1.Controls.
               Remove(dockPanel);
            _dockingManager.RemoveStripButton(this.Dock,
                                              dockPanel.Key);
            _dockedPanelsCount = 0;
         }
         break;
      case DockType.Upper:
      case DockType.LeftSide:
         if (this.DockSplitContainer.Panel1.Controls.
            Contains(dockPanel)){
            remaining =
               (DockablePanel)this.DockSplitContainer.Panel2.
                  Controls[0];
            // Remove the DockablePanel to be undocked
            this.DockSplitContainer.Panel1.Controls.Remove(dockPanel);
            // Change position of the remaining panel-
            // add remaining in Panel1
            // but remove it in panel 2 before
            this.DockSplitContainer.Panel2.Controls.Remove(remaining);
            this.DockSplitContainer.Panel1.Controls.Add(remaining);
            // collapse panel2
            this.DockSplitContainer.Panel2Collapsed = true;
            // the docking Type is now what the panel itself is docked
            remaining.DockingType = GetDockType(this.Dock);
            // now we have the standard padding of a single panel 
            // (no bar for changeing size relation)
            this.DockSplitContainer.Panel1.Padding =
               new Padding(0, 0, 0, 0);
            _dockingManager.RemoveStripButton(this.Dock,
                                              dockPanel.Key);
            if( remaining.PanelState == PanelState.UnpinnedHidden ) {
               // if the remaining is just unpinned and hidden,
               // we hide the controler
               this.Visible = false;
            }
            _dockedPanelsCount = 1;
         }
         break;
      case DockType.Lower:
      case DockType.RightSide:  
         if (this.DockSplitContainer.Panel2.Controls.
            Contains(dockPanel)){
            this.DockSplitContainer.Panel2.Controls.Remove(dockPanel);
            // panel 1 stays still there 
            remaining = (DockablePanel)this.DockSplitContainer.
               Panel1.Controls[0];
            // but its docking Type changed
            DockSplitContainer.Panel2Collapsed = true;
            DockingType = GetDockType(this.Dock);
            this.DockSplitContainer.Panel1.Padding =
               new Padding(0, 0, 0, 0);
            _dockingManager.RemoveStripButton(this.Dock,
                                              dockPanel.Key);
            if (remaining.PanelState ==
               PanelState.UnpinnedHidden) {
               // if the remaining is just unpinned we hide
               // the controler
               this.Visible = false;
            }
            _dockedPanelsCount = 1;
         }
         break;
      case DockType.Center:
         RemovePanelFromTabControler(dockPanel);
         break;
      default:
         return;
   }
   // remove delegate so it doesn't react
   RemoveDelegates(dockPanel);
   dockPanel.SetPinButton(true );
   dockPanel.PanelState = PanelState.Pinned;
   // is still anything hooked? 
   if (UnpinnedPanelsVisibleCount(this.Key ) == 0){
      _mouseHook.Unregister();
   }
   if ( _dockedPanelsCount == 0){
      this.Visible = false;
      // this Controler is closed and disposed so we can remove it
      // from the collection
      this._dockingManager.Admin.RemoveDockControler(this._key); 
      this.Dispose(); 
   }
}

The next problem you need to handle is to determine what will happen if you add a panel to a DockingControler that is visible onscreen, but the DockablePanel it contains is unpinned but visible onscreen. There is no difference to normal docking as long as you are docking to the DockSplitContainer. The unpinned DockablePanel is visible onscreen; therefore, there's no difference if it is pinned or unpinned. You simply have to add your new DockablePanel. But, if you want to dock using DockType.Center, you have defined the rule that if one panel of the TabControler is unpinned, all of the panels need to be unpinned, too. Therefore, in this case, you additionally need to check whether the existing panel or panels are unpinned. If this is the case, you simply unpin them all and add the StripButton.

#region internal methods (DockingControler)
internal void AddPanel(DockablePanel dockPanel){
//. . .  no change in all the code until we have Docktype.Center
   case DockType.Center:
      if (_controlsType != ControlerType.Tabbed){
         CreateTabbedDockControler(dockPanel);
         _controlsType = ControlerType.Tabbed;
      } else {
         AddNewTabPage(dockPanel);
         _controlsType = ControlerType.Tabbed;
      }
      _dockedPanelsCount = _tabControler.TabCount;
      if (AnyPanelsUnpinned()) {
         // we correct the Pinbutton so all are unpinned
         for (int i = 0; i < _dockedPanelsCount; i++) {
            // if one is unpinned we unpin them all
            DockablePanel testPanel =
               (DockablePanel)_tabControler.TabPages[i].Controls[0];
            if (testPanel.PanelState == PanelState.Pinned) {
               testPanel.SetPinButton(false);
               // we add a stripButton
               _dockingManager.ShowButtonStrip(this.Dock, _key,
                                               testPanel.Key);
            }
         }
      }
      break;
   }

#region private Methods (DockingControler)
private bool AnyPanelsUnpinned() {
   return _dockingManager.Admin.AnyPanelsUnpinned(this.Key);
}

Here, you have to find out whether any DockalePanels are unpinned because if you have at least one unpinned, all of the panels set to the TabControler must be set to a PanelState of unpinned and all the buttons need to be changed to that condition.

#region internal Methods (DockAdministration )
internal bool AnyPanelsUnpinned(string controlerKey) {
   //Looping through all of the docked panels
      foreach (KeyValuePair<string, DockablePanel> kvp in
               DockedPanels) {
         DockablePanel dockPanel = kvp.Value;
         // if the panel is part of a specific DockingControler
         // defined by ist key
         if (dockPanel.DockControlerKey == controlerKey) {
            if (dockPanel.PanelState != PanelState.Pinned) {
               return true;
            }
         }
   }
   return false;
}

And, you need a ShowButtonStrip() method that checks only for a specific PanelKey, instead of checking for a list of PanelKeys, which you already did. First, you find out which ToolStrip is used. There, you check whether a button associated to this PanelKey is already added to this ToolStrip. If the key doesn't exist, you have to add a new StripButton. In every case, the ToolStrip is made visible in the end. And here it is.

internal void ShowButtonStrip(DockStyle dockStyle,
                              string controlerKey,
                              string panelKey) {
   direction dir;
   ToolStrip buttonStrip = GetButtonStrip(dockStyle, out dir);
   if (buttonStrip != null) {
      if (IsNewKey(buttonStrip, panelKey)) {
         ToolStripButton stripButton = CreateToolStripButton(dir,
            controlerKey, panelKey);
         stripButton.MouseHover += StripButton_MouseHover;
         buttonStrip.Items.Add(stripButton);
      }
      buttonStrip.Visible = true;
   }
}

Now. you are ready to use your DockablePanels. You now can pin, unpin, and close panels, as well as undock them, even when they are unpinned. Next time, you will learn how to change the caption of a DockablePanel and the associated texts at runtime. You will see that Delegates will handle situations where you have a broad variety of possibilities in a very simple way. Also, you have to work on handling the focus among all of our DockablePanels. And, last but not least, in one of the next articles, you will need to store and reload the configuration of different DockablePanels.

There is one point that might come to your mind: How will it be possible that your DockingManager is able to re-create instances of DockblePanels even when you cannot know how they ever would be designed by a programmer who uses your DockingManager and DockableForms?

The answer is that you are using reflection. Caught your interest? Be with me in the next few articlees. You have gone a long way now, but the full control should be ready soon.

References

  • The definition of Hooking is found in Wikipedia here: http://en.wikipedia.org/wiki/Hooking
  • The whole technique of hooking I have studied in the great book: Subclassing & Hooking with Visual Basic by Stephen Teilhet. ISBN: 0-596-00118-5 This book also covers Subclassing & Hooking in VB.net Thanks to this author for his very detailed explanations.


About the Author

Johann Schwarz

I first encountered computers in 1968, since then, I've written several programs in various Languages such as FORTRAN, C, C++, VB 6, and C#. Mostly I am a hobbiest, self taught programmer, but my computer and my programming is my second life

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

  • "Security" is the number one issue holding business leaders back from the cloud. But does the reality match the perception? Keeping data close to home, on premises, makes business and IT leaders feel inherently more secure. But the truth is, cloud solutions can offer companies real, tangible security advantages. Before you assume that on-site is the only way to keep data safe, it's worth taking a comprehensive approach to evaluating risks. Doing so can lead to big benefits.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds