Creating a DockablePanel-Controlmanager Using C#, Part 4

Creating a DockablePanel-Controlmanager Using C#, Part 4

In Parts 1-3, you have learned how to create all the parts that are needed to create a DockingManager; you now are able to manage DockableForms on the screen. You have laid out the design and learned how the different parts will work together. You decided to use DockingButtons to offer the user the ability to dock the DockableForms. Apart from that, you have learned that, by using the given design, you are faking a bit, because you really do not dock the forms but instead dock a Panel named DockablePanel You have come to the point where you are able to show these certain buttons on the screen when you start to drag your DockablePanel. If you haven't read the previous parts of this article series, you need to do so now before you can start with this one, or you will not fully understand the structure and logic of this design pattern. Here are the preceding 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 before; it will save you hours of work.

Hiding the DockingButtons

If you have followed these articles, you probably are burning to get the next point done. After you hold the MouseButton down to drag around a DockablePanel, the DockableButtons appear on the screen, but they do not leave when you stop dragging the Panel around. So, you need to make these buttons invisible when the left MouseButton is up again. Until now, you had the following code in the MouseUp Delegate.

#region Delegates (DockablePanel)
private void HeaderBox_MouseUp(object sender, MouseEventArgs e) {
   if (e.Button == MouseButtons.Left) {
      _isMouseDown = false;
   }
   OnMouseUp(e);
}

This obviously doesn't influence the DockingButtons. You need to switch them off where you had switched them on, obviously. This has to be done in the DockingManager. You do this by throwing an event that will have its corresponding delegate in the DockingManager. Therefore, you add a new Delegate definition to the DockingControls namespace in DockablePanel.cs.

internal delegate void CapturedDockControlEventDelegate
   (ControlDockedEventArgs e);

and to the DockablePanel itself, you add an event:

internal event CapturedDockControlEventDelegate
DockControlActivities;

Now, you can fire the event when the left MouseButton is released. The required EventArgs class you already designed in Part 3.

#region Delegates (DockablePanel)
private void HeaderBox_MouseUp(object sender, MouseEventArgs e) {
   if (e.Button == MouseButtons.Left) {
      //_dontDeactivate = true;
      _isMouseDown = false;
      _draggingState = DraggingState.Dropped;
      ControlDockedEventArgs de = new ControlDockedEventArgs();
      // informs about dropping ( mouse Up )
      de.DockControlState = _draggingState;
      OnDockControlActivities(de);
   }
   OnMouseUp(e);
}

The method for firing the event is standard to get it fired only when a delegate is connected to it.

#region Methods (DockablePanel)
private void OnDockControlActivities(ControlDockedEventArgs e){
   if (DockControlActivities != null) {
      DockControlActivities(e);
   }
}

To use the delegate in the DockingManager, you have to add a delegate in the DockingManager class to the DockablePanel. You do that by using the same method you already have used before for the ShowDockControlsEventDelegate. You add it when the DockablePanel is added to the DockingManager and you create the required delegate.

#region Internal Methods (DockingManager)
internal void AddPanel(DockablePanel dockPanel) {
   string key = "P" + _count.ToString();
   // each DockablePanel gets a new number 
   // independent from other panels getting closed again.
   // we start with "P0" p for panel
   _count++;
   dockPanel.Key = key;
   AllDockPanels.Add(key, dockPanel);
   dockPanel.DockControlActivities +=
      new CapturedDockControlEventDelegate(dockPanel_Messages);
   dockPanel.ShowDockControls +=
      new ShowDockControlsEventDelegate(dockPanel_ShowDockControls);
   dockPanel.PanelClosing +=
      new DockedFormClosingEventDelegate(dockPanel_PanelClosing);
   // now we attach the carrier to the DockablePanel;
   AttachCarrier(dockPanel);
}
#endregion
#region Delegates (DockingManager)
//.. 

private void dockPanel_Messages (ControlDockedEventArgs e){ 

}
#endregion

Now, this delegate will be called every time your left MouseButton is released. Basically, you need that for three different actions that occur when the left MouseButton is released:

  1. DockingButtons and the docking preview (a transparent Window that shows where the docking occurs) need to vanish.
  2. If you are over just one of the DockingButtons when the left MouseButton is released, you are docking your Panel to the corresponding position in the MDI window.
  3. Dragging around is finished without docking if you are not in range of any DockingButton when the left MouseButton is released.

Although the last two points depend on where the mouse actually is positioned during its release, the first point needs to be done in any case.

You also will use the same delegate when the mouse is hovering over a button to show a docking preview depending on which one you are hovering. Therefore, you will need this delegate to differ between these possibilities. For the moment, you do only the following to get the DockingButtons hidden again.

#region Delegates (DockingManager)
private void dockPanel_Messages(ControlDockedEventArgs e) {
   switch (e.DockControlState ) { 
      // other conditions are added a bit later 
      case DraggingState.Dropped:
         // Hide the DockingButtons
         HideDockingButtons();
         break;
   }
}

#region Private Methods (DockingManager)
private void HideDockingButtons(){
   // When hideing the Buttons we also have
   // to stop the blinking Cycle of the buttons
   LeftButton.Mouse_HasLeft();
   LeftButton.Visible = false;
   RightButton.Mouse_HasLeft();
   RightButton.Visible = false;
   TopButton.Mouse_HasLeft();
   TopButton.Visible = false;
   BottomButton.Mouse_HasLeft();
   BottomButton.Visible = false;
   UpperButton.Mouse_HasLeft();
   UpperButton.Visible = false;
   LowerButton.Mouse_HasLeft();
   LowerButton.Visible = false;
   CenterButton.Mouse_HasLeft();
   CenterButton.Visible = false;
   LeftSideButton.Mouse_HasLeft();
   LeftSideButton.Visible = false;
   RightSideButton.Mouse_HasLeft();
   RightSideButton.Visible = false;
}

Compile the code now and you will see that the DockingButtons will vanish when the left MouseButton is no longer pressed.

Creating a DockablePanel-Controlmanager Using C#, Part 4

The DockingPreviewRectangle

While the mouse moves, it sometimes will hover over one of the DockingButtons. If this happens, you want to show a transparent preview window so you can see where the panel will be docked and which size it has. You already have described that in former lessons. To do this, you need to check whether you are hovering one of these DockingButtons and if so, you have to check which one you are actually hovering. Now, you have two possibilities:

If you are hovering:

  • Make the corresponding DockingButton blink and
  • Calculate the new size of DockablePanel; this is the size of the preview window
  • Calculate the position of the preview window
  • Show the preview window

If you are not hovering:

  • Stop blinking (if it was blinking before)
  • Make preview window invisible (if it is still visible)

This is a very simple logic that you can use to start to code instantly. Remember, you already have declared an enumeration, HitState, in the DockingEnums.cs in Part 3 as the following:

internal enum HitState {
   hoverLeft,
   hoverRight,
   hoverTop,
   hoverBottom,
   hoverLeftSide,
   hoverRightSide,
   hoverCenter,
   hoverUpper,
   hoverLower,
   none
};

Now, you need a method to calculate whether you are currently hovering over a DockingButton and, if so, which one it is.

#region Private Methods (DockablePanel)
private HitState HitTest(MouseEventArgs e){
   HitState hitted;
   WinAPI.POINT mousePos;
   mousePos.x = e.X;
   mousePos.y = e.Y;
   // Transform to Screen Coordinates
   APICall.ClientToScreen(this.Handle, ref mousePos);
   // Are we in the range of a DockingButton
   // (we transform from buttonType to HitRange enum) 
   switch (_admin.InRange(mousePos)){
      case buttonType.Left:
         hitted = HitState.hoverLeft;
         break;
      case buttonType.Right:
         hitted = HitState.hoverRight;
         break;
      case buttonType.Top:
         hitted = HitState.hoverTop;
         break;
      case buttonType.Bottom:
         hitted = HitState.hoverBottom;
         break;
      case buttonType.Upper:
         hitted = HitState.hoverUpper;
         break;
      case buttonType.LeftSide:
         hitted = HitState.hoverLeftSide;
         break;
      case buttonType.RightSide:
         hitted = HitState.hoverRightSide;
         break;
      case buttonType.Center:
         hitted = HitState.hoverCenter;
         break;
      case buttonType.Lower:
         hitted = HitState.hoverLower;
         break;
      default:
         hitted = HitState.none;
         break;
   }
   return hitted;
}

To do that, you need to calculate the size and location of all the rectangles that enclose the different DockingButtons and check whether you are in the range of one of the buttons.

All this is calculated in the DockAdministration class _admin. There, you have to check whether the given mouse position on the screen is within one of the DockingButttons rectangles. Remember the size and position of these buttons had been stored in the DockAdministration class in the _buttonRanges array, just before showing them on the screen, so you can use this data to check whether you are in the range of one of these buttons.

#region Internal Methods (DockAdministration)
internal buttonType InRange(WinAPI.POINT pt){
   for (int i = 0; i < 9; i++) {
      if (_buttonRanges[i].Size != Size.Empty &&  pt.x >=
         _buttonRanges[i].Left && pt.x <= _buttonRanges[i].Right
         && pt.y >= _buttonRanges[i].Top
            && pt.y <= _buttonRanges[i].Bottom) {
         return (buttonType)i;
      }
   }
   // if Point is in no buttons range
   return buttonType.none;
}

You may ask why you have two different enumerations for the buttonType and the HitState. This is done for logical reasons only. Calculating and storing button size and location represents different types of buttons. On the other hand, hovering over a button and being in the range of its rectangle is a typically tested condition of the mouse; therefore, name it Hitstate. These different names are used for easier reading and understanding of the program. So, anywhere in the program where you are reading a HitState you will know you are talking about the mouse position and whether it has hit one of the DockingButtons. Reading about buttonType, you know it is about different buttons, but has basically nothing to do if the button is just hit or not. Every time you do your coding, try to be very exact with the terms you use. Otherwise, in bigger projects, you soon will be lost in codeparts that are difficult to understand and you will need extensive comments to read your own code some years later. Self-explanatory terms that are chosen very meticulously and precisely depending on what their usage in the code is are very helpful for code recognition and are a sign of good programming style.

In the DockablePanel class, you have to add a private field to store the actual HitState of the panel. The needed delegate for that event was already added because it is the same you have for hiding the DockingButtons; you have done that before. But, to be sure you did all correctly, look at what the header of our DockablePanel class looks like now.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using DockingBasicControls;
using DockingControls.WinAPI;
using DockingControls.Classes;

namespace DockingControls.Forms {
   #region Enumerations & Delegatedeclarations
      public enum BorderStyle { Fixed2D, Fixed3D, Sizeable2D,
                                Sizeable3D };
      public enum BorderRange { None, Left, Right, Top, Bottom,
                                LeftTopCorner, LeftBottCorner,
                                RightBottCorner, RightTopCorner };
      internal delegate void
         DockedFormClosingEventDelegate(string key);
      internal delegate void
         ShowDockControlsEventDelegate(ShowControlEventArgs e);
      internal delegate void
         CapturedDockControlEventDelegate(ControlDockedEventArgs e);
   #endregion

   internal sealed partial class DockablePanel : UserControl {
   #region events
      internal event DockedFormClosingEventDelegate PanelClosing;
      internal event ShowDockControlsEventDelegate
         ShowDockControls;
      internal event
         CapturedDockControlEventDelegate DockControlActivities;
   #endregion

   #region Fields
      //. . .
      private HitState _hitted;
      // . . .
   #endregion

In the MouseMoves() method, you have to fire the DockControlActivities event but, you also have to ensure that this message is fired only when the dockablePanel is already moving. Note: Capturing is only done once independent of how often MouseMoves() will be called, but the HitTest has to be done during the whole time the panel is moving around; therefore, it needs to be checked each time MouseMoves() is called in the HeaderBox_MouseMove delegate. Therefore, you have different conditional sections in your code.

private void MouseMoves(MouseEventArgs e){
   WinAPI.POINT mousePos;
   // read the actual mouseposition which will change during moving
   mousePos.x = (short)e.Location.X;
   mousePos.y = (short)e.Location.Y;
   // we calculate the global position again 
   APICall.ClientToScreen(HeaderBox.Handle, ref mousePos);
   Moving(mousePos);
   // We are calling ShowDockControls event only in the
   // sequence when capturing occurs in the moment
   if (_draggingState == DraggingState.Captured) {
      ShowControlEventArgs ds = new ShowControlEventArgs();
      // in the moment we are firing the event using
      // an empty class as there is no specific
      // information needed in the moment
      OnShowDockControls(ds);
      _draggingState = DraggingState.Moving;
   }
   // we are only calling this event when the draggingState
   // is 'Moving' so we are sure LeftMouseButton is down,
   // and DockablePanel is Moving captured by the mouse.
   if (_draggingState == DraggingState.Moving) {
      ControlDockedEventArgs de = new ControlDockedEventArgs();
      HitState hitted = HitTest(e);
      de.DockControlState = _draggingState;
      if (_hitted != hitted) {
         de.Hitted = hitted;
         _hitted = hitted;    // remember last state
         OnDockControlActivities(de);
      }
   }
}

Creating a DockablePanel-Controlmanager Using C#, Part 4

The dockPanel_Messages delegate is already placed into the DockingManager, so you only need to add the code to differ between the information it gets from the MouseUp event where you call it to hide the DockingButtons and the MouseMove event where you will give it the information to show the preview Window.

private void dockPanel_Messages(ControlDockedEventArgs e) {
   switch (e.DockControlState ) { 
      case DraggingState.Moving:
         switch (e.Hitted) {
            case HitState.none:
               // we are not hovering any Button
               // so we set them all back in the normal state
               LeftButton.Mouse_HasLeft();
               RightButton.Mouse_HasLeft();
               TopButton.Mouse_HasLeft();
               BottomButton.Mouse_HasLeft();
               LeftSideButton.Mouse_HasLeft();
               RightSideButton.Mouse_HasLeft();
               CenterButton.Mouse_HasLeft();
               UpperButton.Mouse_HasLeft();
               LowerButton.Mouse_HasLeft();
               // we are sending a Status Information
               OnDockStatusChanged("Info_Ready");
               break;
            case HitState.hoverLeft:
               OnDockStatusChanged("Info_DockingLeft");
               // Left Button is hovered; start its blinking
               LeftButton.Mouse_Hovers();
               break;
            case HitState.hoverTop:
               OnDockStatusChanged("Info_DockingTop");
               TopButton.Mouse_Hovers();
               break;
            case HitState.hoverRight:
               OnDockStatusChanged("Info_DockingRight");
               RightButton.Mouse_Hovers();
               break;
            case HitState.hoverBottom:
               OnDockStatusChanged("Info_DockingBottom");
               BottomButton.Mouse_Hovers();
               break;
            case HitState.hoverUpper:
               OnDockStatusChanged("Info_DockingUpper");
               UpperButton.Mouse_Hovers();
               break;
            case HitState.hoverLower:
               OnDockStatusChanged("Info_DockingLower");
               LowerButton.Mouse_Hovers();
               break;
            case HitState.hoverCenter:
               OnDockStatusChanged("Info_DockingCenter");
               CenterButton.Mouse_Hovers();
               break;
            case HitState.hoverLeftSide:
               OnDockStatusChanged("Info_DockLeftSided");
               LeftSideButton.Mouse_Hovers();
               break;
            case HitState.hoverRightSide:
               OnDockStatusChanged("Info_DockRightSided");
               RightSideButton.Mouse_Hovers();
               break;
         }

            ShowHideDockingPreview(e);
            break;
      case DraggingState.Dropped:
            // Hide the DockingButtons
            HideDockingButtons();
            // Dispose the preview window
            KillDockingPreview();
            // Docking the panel 
            DockPanel(e);
            OnDockStatusChanged("Info_Ready");
            break;
   }
}

There shouldn't be too much explanation needed now. Depending on the result of your HitState, you simply use the related button's Mouse_Hovers() method to make the button blink. But, if you aren't hitting any button to be sure you haven't just left one of them, you set all buttons to their normal state. This way, you make sure no one is blinking. Remember, in the button's property, you only set the _enableBlink field to false to not change a buttons visibility, so you can do this for all buttons independent of whether or not they are visible.

There is a new event to be thrown because you want to be able to have the docking status shown in the statusbar of the main application. You will give this possibility, so that someone may use it or not. Therefore, you will add the needed Information text into your Strings resources Table.

Info_Ready Ready
Info_DockingLeft Docking Panel to the Left
Info_DockingRight Docking Panel to the Right
Info_DockingTop Docking Panel to the Top
Info_DockingBottom Docking Panel to the Bottom
Info_DockingCenter Insert Docking Panel
Info_DockingUpper Docking Panel to the Upper Field
Info_DockingLower Docking Panel to the lower Field
Info_DockLeftSided Docking Panel to the Left Side
Info_DockRightSided Docking Panel to the Right Side

Table 1: Resources Strings to be added into StringResource.resx

Note: If you want to localize this application, you additionally will need to add these strings in your own language to your localized resources table and re-create it using the resources generator as described in Part 1 of this article series. Do all the steps that are described there to get a new resources file for your language and replace the old one with the newly created one.

Now, you add the needed delegate and define it on top of the DockingManager.cs. Besides that, you add the relevant namespace for the Localize.GetResourceString() method.

using DockingBasicControls;
// . . .
#region Public Enumerations & Delegates
public delegate void DockStatusChangedEventDelegate(string status);

In the region Events of the Dockingmanager class, you add the needed event.

#region Events (DockingManager)
   public event DockStatusChangedEventDelegate DockStatusChanged;
#endregion

and the method for calling the event is standard. The only small difference is that you are using the Resourcemanager for the needed Information and that you only will fire the event when the status is changing so you need to set a private field named _prevStatus that you set to Info_Ready because capturing a panel normally is far away from a DockingButton so it causes the status of Info_Ready. Because this status is your default, you get the first change of it when you hover one of the buttons.

Now, do the following code.

#region Fields (DockingManager )
   private string _prevStatus = "Info_Ready";
   //. . .
#endregion 

#region Private Methods ( DockingManger )
private void OnDockStatusChanged(string status) {
   // we only fire when a status is changed
   if (status != _ prevStatus) {
      if (DockStatusChanged != null) {
         string message = Localize.GetResourceString(status,
     "DockingControls", "StringResource");
         DockStatusChanged(message);
      }
   }
   _ prevStatus = status;
}

For testing reasons, you will get this messages visible in your DockingControlTestApp. There, you simply add the DockStatusChanged event delegate as the third line in the MDIForms Constructor, so you now have:

#region ctor MDIForm (DockingControlTestApp)
public MDIForm() {
   InitializeComponent();
   dockManager.CreateAllElements();
   dockManager.DockStatusChanged +=
      new DockStatusChangedEventDelegate(dockManager_DockStatusChanged);
}

The delegate is very simple because it only needs to show the statustext you get fired from the DockingManager.

#region Delegates DockingControlTestApp)
private void dockManager_DockStatusChanged(string status) {
   // we show the text in our Statusbar
   this.tsStatusText.Text = status;
}

Creating a DockablePanel-Controlmanager Using C#, Part 4

Don't forget, if not done before, you need to rename the first ToolStrip statuslabel of the statusbar to tsStatusText, as you can see in the code above. To get this working, you also need to add the namespace DockingControls on top of MDIForm.cs so by now you should have lines of code like that.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using DockingControls.Forms;
using DockingControls; 

namespace DockingControlTestApp {
//. . .

Now, you need to get the docking preview working. As told in former explanations, you need a transparent form for this purpose. To better understand the following calculations, see Figures 1 and 2:

[DefiningSizesLeft.JPG]

Figure1: Caculating preview size when docking to the left

[DefiningSizesTop.JPG]

Figure2: Caculating preview when docking to the top

You have already designed the TransparentForm in Part 1, so you are ready to use it.

#region Fields (DockingManager)
//. . .
private TransparentForm DockingPreviewRectangle =
   new TransparentForm();
//. . .
#endregion

#region Private Methods (DockingManager)
private void ShowHideDockingPreview(ControlDockedEventArgs e){
   int frameLeft = 0;
   int frameTop  = 0;
   KillDockingPreview();
   switch (e.Hitted) {
      case HitState.hoverLeft:
         // left docking means width of form will not change,
         // height is expanded
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _admin.ActDockPanelSize.Width;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height;
         frameLeft = _innerClientScreenRectangle.Left;
         frameTop = _innerClientScreenRectangle.Top;
         break;
      case HitState.hoverRight:
         // right docking means width of form will not change,
         // height is expanded
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _admin.ActDockPanelSize.Width;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height;
         // define the right upper corner of the transparent form
         frameLeft = _innerClientScreenRectangle.Right -
            _admin.ActDockPanelSize.Width;
         frameTop = _innerClientScreenRectangle.Top;
         break;
      case HitState.hoverTop:
         /* Top docking means width of form will be expanded
            to full size, height is reduced to half of the
            innerClientScreenRectangle */
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _innerClientScreenRectangle.Width;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height / 2;
         // define the right upper corner of the transparent Form
         frameLeft = _innerClientScreenRectangle.Left;
         frameTop  = _innerClientScreenRectangle.Top;
         break;
      case HitState.hoverBottom:
         /* Bottom docking means width of form will be expanded
            to full size, height is reduced to half of the
            innerClientScreenRectangle */
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _innerClientScreenRectangle.Width;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height / 2;
         // define the right upper corner of the transparent Form
         frameLeft = _innerClientScreenRectangle.Left;
         frameTop  = _innerClientScreenRectangle.Bottom -
            DockingPreviewRectangle.Height;
         break;
      // other HitStates we will calculate later
      default:
         // this includes hitState.none
         KillDockingPreview();
         // we are finished here
         return;
   }
   // we first need to show  the transparent form
   // afterwards we can move it to its position
   DockingPreviewRectangle.Show();
   DockingPreviewRectangle.Left = frameLeft;
   DockingPreviewRectangle.Top = frameTop;
   DockingPreviewRectangle.Visible = true;
}

Deleting the DockingPreview is simple. You check only whether it still exists and, if so, you check whether it already has been disposed or is in a state of just being disposed. If both conditions are fine, you are able to dispose the form.

private void KillDockingPreview() {
   if (DockingPreviewRectangle != null &&
      DockingPreviewRectangle.Disposing != true) {
      try {
         DockingPreviewRectangle.Close();
         DockingPreviewRectangle.Dispose();
      } catch {
      }
   }
}
//. . .
#endregion

Now, you need to know the size of the actual DockablePanel. As you can see, you will get it from the DockAdministration class where you need to store it, because all values you need for calculation have to be stored there. Because you may have different DockablePanels on the screen, you need the size of the actual one.

To access this value, you first need to create a property for it.

#region Fields( DockAdministration )
//. . . 
private Size _actDockPanelSize;
//. . . 
#endregion 

#region Properties ( DockAdministration )
internal Size ActDockPanelSize {
   get {
      return _actDockPanelSize;
   }
   set {
      _actDockPanelSize = value;
   }
}
//. . . 
#endregion

You now are able to store and use this value in DockAdministration. But, where does DockAdministration get the actual panel's size? See how to manage that. The actual panel is obviously that panel that is just captured. So, you have to add a line to the MouseMoves() method in the conditional section where DraggingState.Captured is true to store the panel's size to the DockAdministration class as the actual panel's size.

#region private Methods (DockablePanel)
//. . . 
private void MouseMoves(MouseEventArgs e){
   //. . .
   if (_draggingState == DraggingState.Moving) {
      ShowControlEventArgs ds = new ShowControlEventArgs();
      _admin.ActDockPanelSize = this.Size;
      // in the moment we are firing the event using an empty class
      OnShowDockControls(ds);
      _draggingState = DraggingState.Moving;
   }
   //. . .
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 4

Now, all is done to get the preview working. The next step will be to create the docking procedure itself; this is done by the DockPanel() method. It's the last instruction to be called by the dockPanel_Messages() delegate. Create an empty method for this now.

#region Private Methods ( DockingManger )
private void DockPanel(ControlDockedEventArgs e) { 

}
//. . .
#endregion

After having done this, you should be able to compile without any problems and get your program to run like Figure 3 shows.

[PreviewFrameResult.jpg]

Figure 3: The result of our code until now. Moving around showing DockingButtons and DockingPreviewRectangle.

Trying to move around the DockblePanel very fast just around a button may lead to some flicker because the transparent form turns on and off. This causes some visual noise as well anywhere on the screen, because the form is created and then moved to its position. You can correct that problem by setting an invisible startposition; therefore, you change the properties of the DockableForm as shown in Table 2. For the transparent Forms properties, you add the following now:

Property Setting
Location -30000;-30000
StartPosition Manual

Table 2: additional data to add to Transparent Forms Properties

When reading my code, you may have wondered why I delete the transparent form before creating a new one. Doing it another way has caused me to have undeleted transparent forms hanging around on the screen when I moved around a DockingButton very quickly and often without having this method included there. Now you might say, who will ever use it that way. Well, never say never. If you want to get a program really working you have to test it in all possible and impossible ways; otherwise, someone will try to get it to crash.

The Real Docking Procedure

Now, after all these necessary preparations are working, you are ready to do the docking itself, as previously described in Part 2.

You already have created the method-stub, so you only have to fill it now. Going through all that when the design was done before in an early stage shouldn't be to complicated; you simply have to express the designpattern you already have in mind, documented with some drawings and some concept files written down for personal use.

#region Private Methods ( DockingManger )
private void DockPanel(ControlDockedEventArgs e) {
   DockablePanel dockingPanel = GetDockingPanel(e.DockingFormKey);
   if (dockingPanel != null) {
      switch (e.Hitted) {
         case HitState.hoverLeft:
            SimpleDock(dockingPanel, DockType.Left);
            break;
         case HitState.hoverRight:
            SimpleDock(dockingPanel, DockType.Right);
            break;
         case HitState.hoverTop:
            SimpleDock(dockingPanel, DockType.Top);
            break;
         case HitState.hoverBottom:
            SimpleDock(dockingPanel, DockType.Bottom);
            break;
         default:
            break;
      }
   }
}
//. . .
#endregion

This way, you are dividing your problem in two smaller problems. Those who already know me aren't wondering about that; this is my usual way to do things. The first thing you need to do is regain the dockablePanel; the other task is docking it in the right way.

Even so, you have nine ways to dock, known from the different HitStates and the description in Part 3; I only discuss SimpleDock at the moment. You see, all the time while going through these articles, I'm trying to reduce all the code to that part you just need to get it working, even if you have to add more and more code to the same methods then and they become more and more complex this way. Because I have written these articles for relatively untrained people, I think this is the best way to follow the 'red thread' of the design concept, by doing only just what is needed to get it working. The SimpleDock() method is that one when only one DockablePanel exists, you are docking to one DockingControler.

So first, look at how to regain the DockablePanel. The known way to get the sender object that is well known and often used, would be to send this information as object itself in the event. You all have seen this often; it looks like this:

// This is only an example; don't code it
private void myControl_MouseMove(object sender, MouseEventArgs e)

where sender is the object to reference the object where the event was fired from. For many reasons, you are going another way. Consider you were the chief of a company and you would have to manage a lot of workers and you don't know all of them personally, but you may have a register where all these workers are listed; you can look them up and then call them by their name and they will do what you want. The same is true here. I haven't talked too much about why you need a SortedList of AllDockPanels and you have added all the panels there, but you did. Think of this list simply as one of the lists the manager needs to call his DockablePanels; that is about it.

Now, you use this list to re-create the DockablePanel object's address to use it.

In the DockablePanels MouseUp method, you need to provide the 'name' of the panel that is expressed by its key. Hence, you add the _key information there. Also, for the docking process you need to give information if you may dock and where.

#region Delegates( DockablePanel)
private void HeaderBox_MouseUp(object sender, MouseEventArgs e) {
   if (e.Button == MouseButtons.Left) {
      //_dontDeactivate = true;
      _isMouseDown = false;
      _draggingState = DraggingState.Dropped;
      ControlDockedEventArgs de = new ControlDockedEventArgs();
      // informs about dropping ( mouse Up )
      de.DockControlState = _draggingState;
      // where we are docking ( if we are docking )
      de.Hitted = _hitted;
      // needed to identify the DockingForm
      // we are adding the key information to the message
      de.DockingFormKey = _key;
      OnDockControlActivities(de);
   }
   OnMouseUp(e);
}
//. . .
#endregion

Now, you are able to retrieve the panel from the SortedList AllDockPanels. TryGetValue includes a check whether the key is available and will result in a null value output if it fails. This way, you avoid getting exceptions.

#region Private Methods ( DockingManger )
internal DockablePanel GetDockingPanel(string key) {
   DockablePanel dockPanel = null;
   AllDockPanels.TryGetValue(key, out dockPanel);
   return dockPanel;
}
//. . .
#endregion

Now, create the SimpleDock method. Basically, to dock the panel, you have to detach the panel from its carrier, the dockableForm. Here, I want to add some small changes to my former code because I have talked with some friends about the project and it came up that I have used the Dockable Forms Tag property to keep the DockablePanels key to know later which carrier was attached to which form. But, that way has disadvantages, because a programmer, not knowing that the Tag property is internally used, may only use the DLL of the DockingManager without looking at the source, will get into trouble when he uses this Tag property. Therefore, you have will also to create a Key property for the DockableForm.

#region Fields ( DockableForm)
// . . .
private string _key;
// . . .
#endregion

#region Properties (DockableForm)
[Browsable(false)]
internal string Key {
   get { 
      return _key;
   }
   set {
      _key = value;
   }
}
// . . .
#endregion

Make sure you keep this Key as an internal so it cannot be used from outside the DockingControls.dll. In the ChangeCarrierStyle() method, change the Form class to DockableForm so that you finally have the following lines (only showing the changes).

#region Private Methods (DockingManager)
private DockableForm ChangeCarrierStyle(DockablePanel dockPanel) {
   DockableForm carrierForm = dockPanel.Carrier;
   // . . .
   // and instead of Tag we now use the new internal Key Property
   carrierForm.Key = dockPanel.Key;
   // . . .
}
//. . .
#endregion
Note: You also have changed the return value of the method from Form to DockableForm. Therefore, you need to change this in the calling method as well.
#region Private Methods (DockingManager) 

private void AttachCarrier(DockablePanel dockPanel) {
   DockableForm carrierForm = ChangeCarrierStyle(dockPanel);
   // . . .
}
//. . .
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 4

The Sorted List then needs to collect DockableForms instead of Forms.

#region Fields (DockingManager)
SortedList<string, DockableForm> AllCarriers =
   new SortedList<string, DockableForm>();
//. . .
#endregion

In DockablePanel, you also have to change the Carrier Property now.

#region Fields (DockablePanel)
   private DockableForm _carrierForm;
   //. . .
#endregion

#region Properties (DockablePanel)
// . . .
internal DockableForm Carrier {
   get {
      return _carrierForm;
   }
   set {
      _carrierForm = value;
   }
}
//. . .
#endregion

In the same manner that you retrieved the DockablePanels, youalso can create a method to have safe access to the carrier.

#region Private Methods (DockingManager)
internal DockableForm GetCarrier(string key) {
   DockableForm carrier = null;
   AllCarriers.TryGetValue(key, out carrier);
   return carrier;
}
//. . .
#endregion

and in dockPanel_isClosing, you change from

#region Delegates (DockingManager)
private void dockPanel_PanelClosing(string key) {
   //. . .
   Form carrierForm = AllCarriers[key];
   AllCarriers.Remove(key);
   // close the carrier ( DockableForm )
   carrierForm.Close();
}
//. . .
#endregion

to a more secure access.

#region Delegates (DockingManager)
private void dockPanel_PanelClosing(string key) {
   //. . .
   DockableForm carrierForm = GetCarrier(key);
   AllCarriers.Remove(key);
   // close the carrier 
   try {
      carrierForm.Close();
      } catch {   }
}
//. . .
#endregion

Now, you are ready to go further on to do your SimpleDock() method. First, look at the the code where I added a minimum of explanations so that you can see what is done.

The created DockingControler cannot be seen on the screen at creation time because this control is not attached to any controls collection at the moment. Additionally, you have set it to invisible in its constructor, as you will see later in the DockingControlers code.

#region Private Methods (DockingManager)
private void SimpleDock(DockablePanel dockingPanel, DockType how) {
   DockableForm carrierForm = GetCarrier(dockingPanel.Key);
   // First we hide Carrier
   carrierForm.Visible = false;
   // then we detach the DockablePanel from Carrierform
   // to do this we simple remove it from the DockbleForms
   // Controls Collection
   carrierForm.Controls.Remove(dockingPanel);
   // we note in the DockablePanel the Form is no longer attached
   dockingPanel.CarrierAttached = false;
   // we also note the DockType there 
   dockingPanel.DockingType = how;
   // now let's create a DockingControler
   DockingControler panelDockControler = new DockingControler();
   // we need to add a significant key to the controler
   panelDockControler.Key = _controlerCount.ToString();
   _controlerCount++;
   // Adding the DockingManagers adress to the DockingControler
   panelDockControler.DockingManager = this;
   // Adding The DockingControler to the MDIForms Controls
   // Collection
   Parent.Controls.Add(panelDockControler);
   // Calculating the controlers Location
   Rectangle controlRec = _admin.CalculateControlerLocation(how);
   panelDockControler.Location = controlRec.Location;
   //Bring Controler to Top position
   panelDockControler.BringToFront();
   // adapting Controlers size
   panelDockControler.Size = controlRec.Size;
   // Docking the controler
   panelDockControler.Dock = GetDockStyle(how);    // new
   // Now adding the DockablePanel to this DockingControler
   panelDockControler.AddPanel(dockingPanel);
   // Making this DockingControler Visible
   panelDockControler.Visible = true;
   // Adding Data to the DockAdministrationclass for
   // future calculations, which needs to take account of this
   // additional DockingControler and its DockablePanel now
   _admin.AddDockedPanel(dockingPanel.Key, dockingPanel);
   _admin.AddDockControler(panelDockControler.Key,
                           panelDockControler);
}
//. . .
#endregion

This is the code. Now, you need to get this working. The first steps are really easy. You need to add some private fields and properties. To create a key for your DockingControler, you simply count the number of them created since starting the program. This counts up and up, even if in-between some DockingControlers were deleted. So, you need to take care of the fact that, by using the solution in a program, after a while maybe some keys may no longer exist because the DockingControlers belonging to that keys have already been closed. But, that is only one point to know when doing design decisions that sort of list, collection, array, dictionary, or whatever else should be used to store the DockingControlers. Add the following things. (Have a look at the regions and in which classes they are used as well as where each region begins and ends or you will mess up your code.)

#region Fields (DockingManager)
private long _controlerCount = 0;
//. . .
#endregion

#region Fields (DockingControler)
private DockingManager _dockingManager;
private string _key;
#endregion

#region Properties (DockingControler)
internal string Key {
   get {
      return _key;
   }
   set {
      _key = value;
   }
}

[Browsable(false)]
internal DockingManager DockingManager {
   // we only need to write to this property
   set {
      _dockingManager = value;
      }
}
//. . .
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 4

After you have added this by using the standard ways to add properties, you now are able to store specific keys in your DockingControlers and the DockingControler is able to address the DockingManager by its pointer stored in the _dockingManager field. Now, you need to do some calculation to get the size and the location of this newly created DockingControler. This, as you already know, will be done in the DockAdministration class, so you need to create the method-stub. Add the needed code.

#region Methods (DockAdministration)
internal Rectangle CalculateControlerLocation(DockType how) {
   // Screen related positions
   int top = _innerClientScreenRectangle.Top;
   int left = _innerClientScreenRectangle.Left;
   Size controlSize = SizeDockingControler(how);
   WinAPI.POINT pt = new POINT();
   switch (how) {
      case DockType.Left:
      case DockType.Top:
         pt.x = left;
         pt.y = top;
         break;
      case DockType.Right:
         pt.x = _innerClientScreenRectangle.Right -
            controlSize.Width;
         pt.y = top;
         break;
      case DockType.Bottom:
         pt.x = left;
         pt.y = _innerClientScreenRectangle.Bottom -
            controlSize.Height;
         break;
   }
   APICall.ScreenToClient(_dockManager.Parent.Handle, ref pt);
   return new Rectangle(pt.x, pt.y, controlSize.Width,
                        controlSize.Height);
}

I hope this is easy to understand. The top and left positions are relative to the screen's left top corner; you can see that because the fieldname contains 'Screen'. By the way, this is a good example of how fieldnames in bigger projects assist you in knowing what values are stored in them. Although _innerClientRectangle would be a private field containing the innerClient's rectangle data related to the Clients Parent, _innerClientScreenRectangle is related to the screen. Using a system like that helps a lot to read and understand a code.

But, back to the above method. The location where you are docking depends on this rectangle and on the fact of whether you dock left, right, top, or bottom. All DockingControlers use the basic docking methods only! Even if you are doing Advanced docking with your DockingPanels, the DockingControlers that are used for this always use basic docking methods. This is one of the points; if someone wants to expand the possibilities of my code to the full range of possibilities and uses it from the IDE, he needs to change the design starting at least at this point. But, you don't do that in these articles; you will still have enough possibilities for advanced docking features, I think.

So, you see that docking left or top both use the left-top corner of your _innerClientScreenRectangle; for right docking, you use the right-top corner but must subtract the width of your DockingControler to get the left-top corner position of your DockingControler, which is always used as the location of a panel. See Figure 4 to better understand these relations.

[MeasurementCalculation.jpg]

Figure 4: Calculating the location of a DockingControler docked to the right.

To completely understand this, look at the following drawing to compare the difference between a DockingControler that is docked to the left or right and another one that is docked to the top or bottom. As you can see, these all are screen coordinates and this way you are totally independent from any position of the ParentForm on the screen and where your innerClientScreenRectangle is positioned, in that MDI Form of the main application. The innerClientScreenRectangle has already been calculated while showing the preview Rectangle, so you don't need to do this again. You may ask why you generally calculate this because you are using the Windows docking mechanics at least a few lines later in your SimpleDock code. Look at the following code. (This has already been done before, so do not add this code again).

#region Private Methods (DockingManager)
private void SimpleDock(DockablePanel dockingPanel, DockType how) {
   //. . .
   // We add this panel to the MDIForm 
   Parent.Controls.Add(panelDockControler)
   // Now the Location of the controlerpanel needs
   // to be related to this parent
   // We calculate Location and size here
   Rectangle controlRec = _admin.CalculateControlerLocation(how);
   // we get the controler into its correct position on the screen
   panelDockControler.Location = controlRec.Location;
   //Bring Controler to Top position
   panelDockControler.BringToFront();
   // adapting Controlers size
   panelDockControler.Size = controlRec.Size;
   // Docking the controler
   panelDockControler.Dock = GetDockStyle(how); 
   //. . .

This explains that, when you want to set the Location of the DockingControler, why after doing the calculation you have transformed the coordinates to MDIForm related values (DockingManagers parent is the MDI Form on the one side, but on the other side, if you don't set this location a very interesting point is that, although the DockingControler is invisible from the beginning and will be set to visible after being docked, you will get a flicker on the screen for a very short moment, showing the DokingControler anywhere on the screen. If you calculate its position, setting it to where exactly he is positioned and then docking it, your program runs very smooth without any flickering. This is the reason why you have calculated this position; it's easy to do and creates a good improvement of the whole system.

As you can see, the method also needs to calculate the size of the DockingControler. This is done by the following method. Here, you use actualDockPanelSize again so you get an idea why you have centralized all the calculations into one class rather than to do it in different places anywhere in the program. All what is required for these calculations can be found here in the DockAdministration; therefore, it is easy to do.

#region Fields(DockAdministration)
   // There is a padding set to the DockblePanel, which is needed
   // to size the Controler using the mouse.
   const int PAD_RIGHT = 5;
   //. . .
#endregion

#region Methods (DockAdministration)
private Size SizeDockingControler(DockType dockType) {
   // we need to size the controler to the size we need
   // e.g. vert controls needs to be sized horizontal
   Size controlSize = new Size();
   switch (dockType) {
      case DockType.Left:
      case DockType.Right:
         controlSize.Width  = _actDockPanelSize.Width + PAD_RIGHT;
         controlSize.Height = _innerClientScreenRectangle.Height;
         return controlSize;
      case DockType.Top:
      case DockType.Bottom:
         controlSize.Width  = _innerClientScreenRectangle.Width;
         controlSize.Height = _innerClientScreenRectangle.Height / 2;
         return controlSize;
  }
  return Size.Empty;
}

The padding I talk about in this code is needed because you may need to resize the dockingControler itself for some reason; therefore, you should be able to grasp the border of the DockingControler using the mouse.

The next thing to do is a simple transformer between two very similar enumerations. The System.Windows:Forms.Dockstyle enumeration is defined like this:

// Don't code this, its defined in the namespace already
public enum DockStyle {
   None,
   Top,
   Bottom,
   Left,
   Right,
   Fill,
   }
//While DockType is defined (we have it defined already)
public enum DockType {
   None      = 0,
   Top       = 1,
   Bottom    = 2,
   Left      = 3,
   Right     = 4,
   Fill      = 5,
   Upper     = 6,
   Lower     = 7,
   LeftSide  = 8,
   RightSide = 9,
   Center    = 10,
}

Now, you can transform DockTypes to DockStyles. As long as basic docking operations are used, you simply set the DocStyle to None if an error occurs and an advanced docking enumeration should be translated.

#region Private Methods (DockingManager)
private DockStyle GetDockStyle(DockType type) {
   if (type <= DockType.Fill) {
      return (DockStyle)type;
   }
   else {
      return DockStyle.None;
   }
}

Creating a DockablePanel-Controlmanager Using C#, Part 4

That's all to this transformation. Easy, isn't it? Now, there are two administrative actions left that you need to do in the SimpleDock method. They are needed to calculate the more advanced Docking cycles later on. We need to add the dockingControler and the DockingPanel to lists in the DockAdministration so you can use their size and Dockingdata whenever you need them.

// To the namespaces of the DockAdministration we add
using DockingControls.Forms;

#region Fields (DockAdministration )
private SortedList<string, DockablePanel> DockedPanels =
   new SortedList<string, DockablePanel>();
// the following SortedList we should already have added before
// as we have needed it for calculations
private SortedList<string, DockingControler> DockControlers =
   new SortedList<string, DockingControler>();
//. . .
#endregion

#region Methods (DockAdministration )
internal void AddDockedPanel(string key, DockablePanel panel) {
     DockedPanels.Add(key, panel);
}

internal void AddDockControler(string key, DockingControler ctl) {
     DockControlers.Add(key, ctl);
}

As you know, for different calculations you have had the DockControlers list as your base to calculate the _innerClientScreenRectangle. This list was empty all the time, which was totally correct, as long as you couldn't really dock any DockControler. Now, it has to be filled because you are docking a controller, so you add the DockingControler to this SortedList.

Now, you already have docked the docking controler and have done all the coding for it. There is only one method missing in the docking procedure of SimpleDock(). But, this is the one really doing it: Adding the DockablePanel to the DockingControler. So, create the method-stub for the AddPanel() method of the DockingControler.

To get an idea of what you will have to do in the first step, Figure 5 shows how to set up the DockingControl from the very first time when you created it, to do advanced docking later. Say you have no real basic docking; you always dock in an advanced mode. What you are doing if you only have one panel to dock to a position in SimpleDock() is that you are preparing the DockingControler for advanced docking, but collapse the unneeded panel of your SplitContainer. Therefore, only your DockablePanel and a small padding of the DockingControler can be seen on the screen. Figure 5 should make it easier for you to understand.

[DockingControlerP4.JPG]

Figure 5: Comparing a DockingControler docked to left or right with another one docked to top or bottom.

As mentioned before, you have to make the DockingControler invisible at initialization time, and you also will prepare its BackColor for the time it will get visible. In the constructor, you add:

#region Constructor
internal DockingControler(){
   InitializeComponent();
   // at Initialisation we will stay invisible
   this.Visible =false;
   this.BackColor = SystemColors.ActiveBorder;
}
#endregion

and here is what you need to do in the AddPanel method. Again, you will need to add different fields and properties to get this working. But first, look at the code.

#region internal Methods (DockingControler)
internal void AddPanel(DockablePanel dockPanel) {
   switch (dockPanel.DockingType) {
      case DockType.Left:
      case DockType.Right:
         // All the controls are filled into the dockingControler
         _controlsType = ControlerType.Vertical;
         CreateMultiDockControler(_controlsType, dockPanel);
         _dockedPanelsCount = 1;
         break;
      case DockType.Top:
      case DockType.Bottom:
         // All the controls are filled into the dockingControler
         _controlsType = ControlerType.Horizontal;
         CreateMultiDockControler(_controlsType, dockPanel);
         _dockedPanelsCount = 1;
         break;
   }
}
#endregion

As you see in Figure 5, you basically have two different types of DockingControlers. One is vertically oriented like the left and right docked DockingControler and one that is horizontally oriented like that which are docked to top or bottom. At first, you will have to add a private field for the ControlerType. You will name it _controlsType. This value will store how the DockingControler is used. Additionally, you add the ControlsType property to read the value and by knowing its actual usage and a counter to know how much DockablePanels are docked. This should also be accessable by a read-only property. At last, you add a field that will contain the SplitContainer you can see in Figure 5.

#region Fields (DockingControler)
private ControlerType _controlsType;
private int _dockedPanelsCount;
private SplitContainer DockSplitContainer;
//. . . 
#endregion
#region Properties (DockingControler)
internal ControlerType ControlsType {
   get {
      return _controlsType;
      }
}

internal int DockedPanelsCount {
   get {
      return _dockedPanelsCount;
   }
}
//. . .
#endregion

As you can see, in all these types of basic docking you have only one DockablePanel to add to the DockControler so you have to set dockedPanelsCount to 1. The ControlerType depends on how the panel was docked and in all of these cases of basic Docking, you are calling CreateMultiDockControler(). Don't wonder why this method is still done in the switch statement. All the 'switch' options that will be added for advanced docking use other methods, so you have to set the needed methods into the switch options.

#region private Methods (DockingControler)
private void CreateMultiDockControler(ControlerType type,
   DockablePanel dockPanel) {
   // We create a new SplitContainer object
   this.DockSplitContainer = new SplitContainer();
   // we will dock the DockablePanel by using Dockstyle Fill
   dockPanel.Dock = DockStyle.Fill;
   // The Splitter will also be docked this way
   this.DockSplitContainer.Dock = DockStyle.Fill;
   // Panel2 is collaped (see drawing )
   this.DockSplitContainer.Panel2Collapsed = true;
   // if we have a Horizontal DockingControler
   if (type == ControlerType.Horizontal) {
      this.DockSplitContainer.Size =
         new Size(this.Width, this.Height - PAD_VER );
      switch (this.Dock) {
         case DockStyle.Top:
            this.DockSplitContainer.Location = new Point(0, 0);
            this.Padding = new Padding(0, 0, 0, PAD_VER);
            break;
         case DockStyle.Bottom:
            this.DockSplitContainer.Location =
               new Point(0, PAD_VER);
            this.Padding = new Padding(0, PAD_VER, 0, 0);
            break;
      }

      dockPanel.Size = new Size(this.Width, this.Height - PAD_VER);
      // the Horizontal splitcontroler has a vertical Splitter
      this.DockSplitContainer.Orientation = Orientation.Vertical;
   } else {
      this.DockSplitContainer.Size =
         new Size(this.Width - PAD_HOR, this.Height);
      switch (this.Dock) {
         case DockStyle.Left:
            this.DockSplitContainer.Location = new Point(0, 0);
            this.Padding = new Padding(0, 0, PAD_HOR, 0);
            break;
         case DockStyle.Right:
            this.DockSplitContainer.Location =
               new Point(PAD_HOR, 0);
            this.Padding = new Padding(PAD_HOR, 0, 0, 0);
            break;
      }
      dockPanel.Size = new Size(this.Width - PAD_HOR, this.Height);
      // the vertical Splitcontroler has a horizontal splitter
      this.DockSplitContainer.Orientation = Orientation.Horizontal;
   }
   // Panel1 will be the DockablePanel
   this.DockSplitContainer.Panel1.Controls.Add(dockPanel);
   this.DockSplitContainer.Panel1.Padding = new Padding(0, 0, 0, 0);
   this.DockSplitContainer.Panel1.SetBounds(0, 0,
      dockPanel.Size.Width, dockPanel.Size.Height);
   this.Controls.Add(this.DockSplitContainer);
   this.DockSplitContainer.Name = "DockSplitContainer";
   this.Name = "DockingControler";
}
//. . . 
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 4

This looks very complicated at first glance, but if you look at what I have already explained, you will understand most of it. I have explained most of it in the different lines above. There is only one particular constructural detail that is done in this DockingControler and needs some special explanation: the padding.

[PaddingHorizontalControler.JPG]

Figure 6: The horizontal type of DockingControler with its padding needed to grasp it for manual sizing reasons.

[PaddingVertikalControler.JPG]

Figure 7: The vertical type of DockingControler with its padding needed to grasp it for manual sizing reasons.

Looking at these figures, I think it should not be a problem to understand how this padding is done in my code.

Now, you should be able to compile and test your application. For the first time you will be able to dock your panels.

[DockingFirstTime.JPG]

Figure 8: The first time you are able to dock your DockablePanels.

Now, you dock the first time and, oh my goodness, you are in trouble. Resizing doesn't work; otherwise, the ListView control placed on your TechForm in the Test-Application would have changed its size. Are you wondering why? Don't forget what you learned about that matter. All that you see is fake. The DockablePanel obviously is sized, but it is no longer attached to the DockableForm. It was detached before, when you docked the DockablePanel. Therefore, it cannot know that the DockablePanel is sized, so it is not resized itself and also doesn't throw a resize event. To correct this, you simply need to resize the DockableForm so it becomes the same size as the DockablePanel.

#region Delegates (DockablePanel)
private void DockablePanel_Resize(object sender, EventArgs e) {
   // . . .
   picGripTriangle.Top = filler.Height - picGripTriangle.Height -
      GRIP_PADDING;
   // After this line we add the following
   if( _dockingType != DockType.None) {
      // if we are docked we are not attached to the carrier 
      // so we need to size the carrierForm 
      _carrierForm.Size = new Size(this.Width, this.Height);
   }
}
//. . . 
#endregion

This grants that you are aren't getting in an endless resizing cycle when you are attached to the carrier and the carrier is resized and resizes the DockablePanel and this again would resize the DockableForm as its carrier and so on. You see?

Now it looks much better, because the resizing works. Have a look at it.

[ClosingDockingPaneProblem.JPG]

Figure 9: Resizing works, but closing a DockablePanel, doesn't close the DockingControler.

As you can see in Figure 9, you have handled the resizing problem and the DockableForm also gets resized when it isn't attached to its panel. This works pretty well now. But, whatever you are coding, you have to think and test all possibilities that may happen when using a control and testing whether it works. If not, you need to debug, find the cause, and then adapt your code so it fits all situations you can think of. As all experienced programmers know, this normally is only the beginning of a needed longtime testing and researching cycle, if there are no hidden bugs. In this case, there was no test needed; I only wanted to show you where you are in your coding progress and where to go next. The DockablePanel also needs to inform the DockingControler that it is closing. The DockingControler needs to have a delegate to get this message and needs to decide what to do. Make it simple. If it contains only one DockablePanel and this one is closed now, it has to be disposed too. So, define a delegate and add a PrepareClosing Event to the DockablePanel. Fire this event in the OnClosingPanel() Method just before you fire the PanelClosing event.

The only small difference is that PrepareClosing is only called when the panel is docked, wheras PanelClosing is called in every case.

#region Delegates (DockablePanel)
private void DockablePanel_Resize(object sender, EventArgs e) {
   // . . .
   picGripTriangle.Top = filler.Height - picGripTriangle.Height -
      GRIP_PADDING;
   // After this line we add the following
   if( _dockingType != DockType.None) {
      // if we are docked we are not attached to the carrier
      // so we need to size the carrierForm 
      _carrierForm.Size = new Size(this.Width, this.Height);
  }
}
//. . .
#endregion

In the DockingControler, you now will create the Delegate method and add it to the DockablePanels delegates list so it will be called when the DockablePanel closes. This time, you use an a slightly different method to create delegate objects. You obviously can close only the panels one by one, clicking their closing buttons. You may be quick, but you never will be that quick; this delegate method isn't finished and needs to be called again from another DockablePanel that is placed on the same DockingControler. Hence, you don't need to create lots of delegate objects; you only create one in each DockingControler, which will be added to each DockablePanel when it is added to this DockingControler. Here you go.

#region Fields (DockingControler)
   //.. (after all other fields we add)
   //Delegate adresses
   DockedFormPrepareClosingEventDelagate _prepareClosing;
#endregion

In the Constructor, you initialize that delegate and, as an addition, you also initialize the ControlerType with 'new', which is never used another way than that the value is initialized when the DockingControler is created.

internal DockingControler() {
   InitializeComponent();
   // at Initialisation we will stay invisible
   this.Visible = false;
   this.BackColor = SystemColors.ActiveBorder;
   _controlsType = ControlerType.New;
   // we create one instance of that delegate
   _prepareClosing = new
      DockedFormPrepareClosingEventDelagate(dockPanel_PrepareClosing);
}

Now, for the delegate method, you create a region Delegate in the DockingControler and do the following:

#region Delegates(DockingControler )
private void dockPanel_PrepareClosing(DockablePanel dockPanel) {
   // the counted Panels on this controler will be zero now
    _dockedPanelsCount = 0;
   // we disconnect the delegate from this Dockablepanel
   dockPanel.PrepareClosing -= _prepareClosing; 
   // we hide the DockingControler
   this.Visible = false;
   // this conrtroler no longer will hold panels so it is going
   // to be disposed and removed from the collection
   // This is necessary so it is no longer part of 
   // any calculation there 
   this._dockingManager.Admin.RemoveDockControler(this._key);
   // we destroy the DockingControler
   this.Dispose();
}
//..
#endregion

But, this needs to get this delegate connected to the DockablePanel, so it is able to do its job. This is done when you add the panel to the DockingControler:

#region internal Methods (DockingControler)
internal void AddPanel(DockablePanel dockPanel) {
   // . . .  (as the last line we add)
   // Now adding the Delegates we need
   dockPanel.PrepareClosing += _prepareClosing;
}
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 4

Remember, you have added the DockablePanel as well as the DockingControler to the DockAdministration class to take account of them in your calculations of the _innerClientScreenRectangle and other administrative actions that are done there. When both are disposed, you need to have methods to delete them in the lists where they are stored. So, you create the following very simple methods.

#region Methods DockAdministration
internal void RemoveDockControler(string key) {
   DockControlers.Remove(key);
}

internal void RemoveDockedPanel(string key) {
   DockedPanels.Remove(key);
}
// . . .
#endregion

As the DockingControler is removed in the DockAdministration by the PrepareClosing delegate, the DockablePanel will be removed in the PanelClosing event delegate in the DockingManager. Therefore, you add it there.

#region Delegates (DockingManager)
private void dockPanel_PanelClosing(string key) {
   DockablePanel dockPanel = GetDockingPanel(key);
   if (dockPanel.DockingType != DockType.None) {
      // when the control was docked and is closed now,
      // we need to delete it in the DockedPanels List
      this.Admin.RemoveDockedPanel(dockPanel.Key);
   }
   // It's closed so we remove it from AllDockPanels List too
   AllDockPanels.Remove(key);
   // now we delete its carrier (DockableForm) too
   // read out the Form from our SortedList
   DockableForm carrierForm = GetCarrier(key);
   // Remove it from the SortedList
   AllCarriers.Remove(key);
   // close the carrier (DockableForm)
   try {
      carrierForm.Close();
   } catch {   }
}
// . . .
#endregion

Again, give it a try; you will see the DockablePanels are working and you will be able to close them correctly. However, you cannot undock them and you also are not able to resize the DockingControler and the DockablePanels within.

Conclusion

Now the simple Docking methods that you also have named Basic Docking methods are finished. In the next article, you will go on to change the DockableForms size while they are docked and you will learn how to undock them again as well. Additionally, you will add the needed methods for more advanced docking methods. The attached zipfile contains the project to the point you have reached in-between. I hope you have enjoyed this article and that we will meet each other again the next time.



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

  • It's time high-level executives and IT compliance officers recognize and acknowledge the danger of malicious insiders, an increased attack surface and the potential for breaches caused by employee error or negligence. See why there is extra emphasis on insider threats.

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds