Creating a DockablePanel-Controlmanager Using C#, Part 7

WEBINAR:On-Demand

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

WEBINAR:On-Demand

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.

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:

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.

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

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

WEBINAR:On-Demand

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.

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.

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.

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.

Creating a DockablePanel-Controlmanager Using C#, Part 7

WEBINAR:On-Demand

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.

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.

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

WEBINAR:On-Demand

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.

Creating a DockablePanel-Controlmanager Using C#, Part 7

WEBINAR:On-Demand

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.

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.

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.

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.

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.

Creating a DockablePanel-Controlmanager Using C#, Part 7

WEBINAR:On-Demand

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.

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.

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:

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.

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.

Creating a DockablePanel-Controlmanager Using C#, Part 7

WEBINAR:On-Demand

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.

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.

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.

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

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.

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

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.

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.

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.

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.

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

WEBINAR:On-Demand

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.

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:

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.

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.

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.

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:

Creating a DockablePanel-Controlmanager Using C#, Part 7

WEBINAR:On-Demand

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.

Maybe you will find some more bugs, too

You will find out the error seems to be bound to a very specific case

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

WEBINAR:On-Demand

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.

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:

Creating a DockablePanel-Controlmanager Using C#, Part 7

WEBINAR:On-Demand

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.

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:

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

WEBINAR:On-Demand

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.

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.

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.

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.

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

Advertiser Disclosure:
Some of the products that appear on this site are from companies from which QuinStreet receives compensation. This compensation may impact how and where products appear on this site including, for example, the order in which they appear. QuinStreet does not include all companies or all types of products available in the marketplace.

Thanks for your registration, follow us on our social networks to keep up-to-date