Creating a DockablePanel-Controlmanager Using C#, Part 6

More about Advanced Docking

Welcome to Part 6 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 panes, 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 whole 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 different 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 earlier in former 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 about how to build your own usercontrols.

If you want to follow this article and you haven't read all the other parts yet, you need to 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 articles before, you already built the DockableForm with all the basic docking actions. Then, you created the code to show the preview rectangle and the DockingButtons for all the advanced docking actions too, so the next step is to extend the existing docking actions so the advanced docking may occur.

So, look how this will work. Do you remember how you docked the first DockablePanel? Remember what I explained in Part 4.

Figure 1: The DockingController gets the first DockablePanel docked.

Now, have a look what happens when you add a second DockablePanel to the DockingControler.

Figure 2: The simple way: Inserting the DockablePanel to the lower position in the SplitContainer.

This is simple because you only need to add the DockablePanel to the free slot (Panel2) of the SplitContainer. And then, no longer collapse it, so both panels could be seen. The more complex way is to dock the DockablePanel in the same position where the first panel was docked, the Upper position (Panel 1) of the SplitContainer. This looks like the following:

Figure 3: Docking the second DockablePanel to the Upper Position.

In this case, you first need to remove the existing DockablePanel form the Split Containers Panel1 and add it to Panel 2. Then, you insert the second DockablePanel on Panel1 of the SplitContainer to no longer collapse the Containers Panel2.

I have explained now how this works for a vertical orientated DockingControler that could be docked to the left or to the right. For the horizontal orientated DockingControler used on the Top or Bottom positions, RightSide docking corresponds to Figure 2 and LeftSide docking uses the same methods you see in Figure 3. A totally different method is to implement docking by adding the DockablePanels to a TabControl.

Figure 4: Docking to Center by inserting into a TabControl instead of adding to a SplitContainer.

As you remember, when docking the first DockablePanel, you always dock to the SplitControler. So, to dock the second one on a TabControl, you will need to perform the following steps:

  1. Remove the already docked DockablePanel from the SplitControler.
  2. Remove the SplitControler itself.
  3. Add a TabControl on the DockingControler.
  4. Add the original primordial DockablePanel to the TabControl.
  5. Add the new DockablePanel to the TabControl.

But, if you already have docked two DockablePanels to the DockingControler and will add an additional one, the same Docking method has to do the following.

  • Add the new DockablePanel to the TabControl.
  • Nothing else.

Creating a DockablePanel-Controlmanager Using C#, Part 6

Advanced Docking: Adding to the SplitContainer

This is the logic; now, do it in code. You will add all the pieces of this puzzle piece by piece so that you get your desired product. First, you will complete your DockPanel() method to also contain the Advanced docking actions. The method should now look like the following segment:

#region Private Methods (DockingManager)
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;
         case HitState.hoverUpper:
            AdvancedDock(dockingPanel, DockType.Upper,
                         e.HoverBaseKey);
            break;
         case HitState.hoverLower:
            AdvancedDock(dockingPanel, DockType.Lower,
                         e.HoverBaseKey);
            break;
         case HitState.hoverLeftSide:
            AdvancedDock(dockingPanel, DockType.LeftSide,
                         e.HoverBaseKey);
            break;
         case HitState.hoverRightSide:
            AdvancedDock(dockingPanel, DockType.RightSide,
                         e.HoverBaseKey);
            break;
         case HitState.hoverCenter:
            AdvancedDock(dockingPanel, DockType.Center,
                         e.HoverBaseKey);
            break;
         default:
            break;
      }
   }
}
#endregion
Note: The code I'm explaining in these articles was written/designed more than one year ago, and I have written some thousand lines of other code since then. You may think now that I have a mind like an elephant, by remembering every single line I have written all in my life? Oh no! To be able to explain this all to you, how it is was designed and how it works, I only needed to read and understand the self-written code years after it was done. This is an ability that is achieved by two things. The first is to keep things simple by reducing complex code to some easy to read and understandable methods. The other is to have enough explanations in between the lines of your code, so that you are able to remember what the design was. Both these methods are absolutely necessary to maintain your code years after you have deployed your solution.

Because of this, the above code is very easy to read and I think it needs no special explanations. You only need to define the new method AdvancedDock ().

#region Private Methods (DockingManager)
private void AdvancedDock(DockablePanel dockingPanel, DockType how,
                          string hoverBaseKey) {
   // Adress the DockableForm that contains this DockablePanel
   Form carrierForm = GetCarrier(dockingPanel.Key);
   // Remove Panel from Carrierform (the DockableForm)
   carrierForm.Controls.Remove(dockingPanel);
   // Setting the property so the dockable panel knows
   // it is detached
   dockingPanel.CarrierAttached = false;
   // Hide the Carrierform so there is only the dockablePanel to
   // be seen on the screen
   carrierForm.Visible = false;
   //now adress the DockControler where we will have too dock
   // we have got it's key in hoverBaseKey
   DockingControler panelDockControler =
      GetDockingControler(hoverBaseKey);

   panelDockControler.Visible = false;
   // Adding the DockType which is to be used to the DockablePanel
   // so whe can check how to add the DockablePanel
   // to the correspnding DockingControler
   dockingPanel.DockingType = how;
   // we prevent the MDI Form and the DockingControler to
   // redraw its Layout
   this.Parent.SuspendLayout();
   panelDockControler.SuspendLayout();
   panelDockControler.AddPanel(dockingPanel);
   panelDockControler.Visible = true;
   // now after docking is done we redraw the MDI and
   // it's DockingControler
   panelDockControler.ResumeLayout(false);
   this.Parent.ResumeLayout();
   this.Admin.AddDockedPanel(dockingPanel.Key, dockingPanel);
}
//. . .
#endregion

Because you need to be informed which DockingControler is in use, you need the hoverBaseKey as an input to this method. Looking back, you will see that this input comes from the DockPanel() method that is called in the dockPanel_Messages() delegate. This delegate is called when the DockControlsActivities() event is fired. To dock the DockablePanel, this happens when the LeftMouseButton is released. So, you need to insert this hoverBaseKey data in the HeaderBox_MouseUp() event handler just before the DockControlsActivities() event is fired there.

So, add this one line there now. It gets the key of exactly that DockingControler; you just are hovering from the DockAdministration and pass it to the event arguments, so you will have the needed information in your AdvancedDock() method.

#region Delegates (DockablePanels)
private void HeaderBox_MouseUp(object sender, MouseEventArgs e) {
   if (e.Button == MouseButtons.Left) {
      //. . .
         if (_draggingState != DraggingState.Dropped &&
             _dockingType == DockType.None) {
            //. . .
            de.Hitted = _hitted;
            // just after this line we add
            de.HoverBaseKey = _admin.HoverBaseKey;
  //. . .and we go on as we had it before
}
   }
   OnMouseUp(e);
}
#endregion

Looking into your AdvancedDocking() function, you will find out that your Docking process is again reduced to another already known method, namely the DockingControlers AddPanel() method. This method needs to be expanded because at the moment it only contains the Basic Docking methods. So, you must expand this method now with the Advanced features.

Creating a DockablePanel-Controlmanager Using C#, Part 6

In the AddPanel() of the DockingControler just after the last switch statement case DockType.Bottom is finished, you add the conditions for DockType.Upper, -.Lower, -.LeftSide, and -.RightSide, and you also prepare a switch condition for Docktype.Center but, for an easy to understand implementation, the needed code will be done a bit later.

#region internal Methods (DockingConroler)
internal void AddPanel(DockablePanel dockPanel) {
//. . . we start just after 'break'
   case DockType.Upper:
      _controlsType = ControlerType.Vertical;
      ChangeMultiDockControler(_controlsType, DockType.Upper,
                               dockPanel);
      _dockedPanelsCount = 2;
      break;
   case DockType.Lower:
      _controlsType = ControlerType.Vertical;
      ChangeMultiDockControler(_controlsType, DockType.Lower,
                               dockPanel);
      _dockedPanelsCount = 2;
      break;
   case DockType.LeftSide:
      _controlsType = ControlerType.Horizontal;
      ChangeMultiDockControler(_controlsType, DockType.LeftSide,
                               dockPanel);
      _dockedPanelsCount = 2;
      break;
   case DockType.RightSide:
      _controlsType = ControlerType.Horizontal;
      ChangeMultiDockControler(_controlsType, DockType.RightSide,
                               dockPanel);
      _dockedPanelsCount = 2;
      break;
   case DockType.Center:
      // we will do this a bit later
      break;
}
//. . . the rest of the method follows here

#endregion

And now, you are directly in the DockingControler class and, as explained earlier, you need to change your SplitContainer behavior that currently only contains one DockablePanel and is collapsed at panel2. The ChangeMultiDockControler() method will do this work. To understand this code, you only need to have a look at the comments in the code.

#region private Methods(DockingControler)
private void ChangeMultiDockControler(ControlerType type,
   DockType how, DockablePanel dockPanel) {
   // we are not changing the layout unless it is finished
   this.DockSplitContainer.Panel1.SuspendLayout();
   this.DockSplitContainer.Panel2.SuspendLayout();
   this.DockSplitContainer.SuspendLayout();
   // get the reference to the first panel(it's always in the 
   // first Panel of the SplitContainer)
   DockablePanel firstPanel =
      (DockablePanel)DockSplitContainer.Panel1.Controls[0];
   // Now we adapt the size of the SplitContainer
   AdaptSize(type);
   //Now we insert the DockablePanel to the SplitContainer
   InsertPanel(how, dockPanel, firstPanel);
   // The orientation of the Container depends on the type
   // of the DockingControler
   SetPaddingAndOrientation(type);
   // Now we dock the DockablePanels inside the SplitContainer
   dockPanel.Dock = DockStyle.Fill;
   // At last allow drawing the layout
   this.DockSplitContainer.Panel1.ResumeLayout(false);
   this.DockSplitContainer.Panel2.ResumeLayout(false);
   this.DockSplitContainer.ResumeLayout(false);
}
//. . .
#endregion

The DockingControler uses a bar to drag on it and to size the DockablePanels while they are docked. Its size was already defined earlier in these articles by the constants PAD_VER and PAD_HOR, defining the padding. Remember these drawings:

[PaddingHorizontalControler.JPG]

Figure 5: The design of a DockingControler docked to Top or Bottom.

[PaddingVertikalControler.JPG]

Figure 6: The design of a DockingControler docked to Left or Right.

Here is the code for setting the location and size of your DockSplitContainer.

#region private Methods(DockingControler)
private void AdaptSize(ControlerType type) {
   if (type == ControlerType.Horizontal) {
      this.DockSplitContainer.Size =
         new Size(this.Width, this.Height - PAD_VER);
   }
   else {
      this.DockSplitContainer.Size = new Size(this.Width -
         PAD_HOR, this.Height);
   }
   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;
      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;
   }
}
//. . .
#endregion

In the InsertPanel method you have what I have shown in Figures 2 and 3. The first DockablePanel for 'docking' is always added to the SplitContainer in Panel1.Controls. This is the upper one; your DockingControler is of ControlerType.Vertical and the LeftSide one of the DockingControler is of ControlerType.Horizontal. So, if you want to dock the next DockablePanel into the Upper (or the LeftSide) position, you first need to remove the previous docked DockablePanel from Panel1.Controls and instead of it, add the new one here. Then, add the first DockablePanel, which was originally added in panel1.Controls to Panel2.Controls. This way, the first added panel changes is placed to the Lower or RightSide position and the just added one takes the place of the Upper or LeftSide of the SplitContainer.

On the other side, if you want to add your DockablePanel to the Lower or RightSide, you simply need to add it to Panel2.Controls of the SplitContainer and the first docked DockablePanel stays in its place where it originally was placed. In both cases, Panel2 of the SplitContainer is no longer collapsed. This is the code:

#region private Methods(DockingControler)
private void InsertPanel(DockType how, DockablePanel dockPanel,
                         DockablePanel firstPanel) {
   if (how == DockType.LeftSide || how == DockType.Upper) {
      // remove panel in the first place
      this.DockSplitContainer.Panel1.Controls.Remove(firstPanel);
      this.DockSplitContainer.Panel1.Controls.Add(dockPanel);
      // add panel to the second place
      this.DockSplitContainer.Panel2.Controls.Add(firstPanel);
   }
   else {    //lower or rightSide
      // panel 1 stays as it is, we add panel2
      this.DockSplitContainer.Panel2.Controls.Add(dockPanel);
   }
   dockPanel.DockingType = how;
   firstPanel.DockingType = OppositeType(how);
   this.DockSplitContainer.Panel2Collapsed = false;
}
//. . .
#endregion

It is necessary to store the DockType of each DockablePanel in its DockingType property because this information is needed in different other actions (for example, during undocking or closing one of the 'docked' DockablePanels). I say 'docked' because as I told you long ago in this article series, all this is fake; you are simply adding the DockablePanels to a SplitContainer. To set the DockingType, you know the DockType you are currently docking, and by doing this, the other panel in the same DockingControler will just have to be the opposite type. Understand that, if you dock to the Lower position, the other DockablePanel is in the Upper position. It's very simple to just find out the opposite value of a specific DockType. This is done here.

#region private Methods(DockingControler)
private DockType OppositeType(DockType type) {
   switch (type) {
      case DockType.Upper:
         return DockType.Lower;
      case DockType.Lower:
         return DockType.Upper;
      case DockType.LeftSide:
         return DockType.RightSide;
      case DockType.RightSide:
         return DockType.LeftSide;
   }
   return DockType.None;
} 
//. . .

#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 6

Lastly, you need to set the orientation of your SplitContainer. To start the second panel of the DockingControler on the screen, you set the SplitterDistance to the middle of the Controler. Using padding, you are setting the divider's width to two pixels, so it's a bit easier to grab it with the mouse when you want to move the divider with the mouse. If you want to have the divider in its original configuration, simply set the BAR constant to zero, so that you have no padding.

#region Fields(DockingControler)
   private const int BAR = 2;
   //. . .
#endregion

#region private Methods(DockingControler)
private void SetPaddingAndOrientation(ControlerType type) {
   // the Horizontal splitcontroler has a vertical Splitter
   if (type == ControlerType.Horizontal) {
      this.DockSplitContainer.Orientation = Orientation.Vertical;
      this.DockSplitContainer.SplitterDistance = this.Width / 2;
      this.DockSplitContainer.Panel1.Padding =
         new Padding(0, 0, BAR, 0);
      this.DockSplitContainer.Panel2.Padding =
         new Padding(BAR, 0, 0, 0);
   }
   else {
      this.DockSplitContainer.Orientation = Orientation.Horizontal;
      this.DockSplitContainer.SplitterDistance = this.Height / 2;
      this.DockSplitContainer.Panel1.Padding =
         new Padding(0, 0, 0, BAR);
      this.DockSplitContainer.Panel2.Padding =
         new Padding(0, BAR, 0, 0);
   }
}
//. . .
#endregion

And now, you are able to compile. For the first time, you can do some more advanced docking like this one.

[AdvancedDockingFirstResult.JPG]

Figure 7: The first time you are able to do Advanced docking.

Now you have reached a point where, for the first time, you can see how the DockingManager, the DockingControler, the DockablePanel, and the DockableForm work together. There is still a lot to do. Not only haven't you coded Docktype.Center yet, but also closing one of the panel or undocking one of the panels needs to be adapted to get it working even when you have more then one DockablePanel docked.

Closing the Advanced DockablePanel

The first things you will change are the methods called to carry out closing one of the panels. At the moment, there is only very simple code in the related delegate in your DockingControler. I added some explanations so you can easily see this will only work for closing the last DockablePanel of a specific DockingControler.

#region Delegates(DockingControler)
private void dockPanel_PrepareClosing(DockablePanel dockPanel) {
   // this explains we have no more docked panels 
   // in the DockingControler
   _dockedPanelsCount = 0;
   // So we removed the delegate in the Dockablepanel
   dockPanel.PrepareClosing -= _prepareClosing;
   // Then we set it to invisible
   this.Visible = false;
   // this controler no longer holds panels so it is going
   // to be disposed and removed from the collection
   this._dockingManager.Admin.RemoveDockControler(this._key);
   // we destroy the DockingControler
   this.Dispose();
}
#endregion

As you know, this delegate is called when a DockablePanel is going to be closed. At the moment, the code doesn't differentiate how many panels are added to this DockingControler. This obviously only works when there is one DockablePanel left on a specific DockingControler. You need to add code to only remove DockablePanel from its DockingControler when there are more then one panels added to this DockingControler and only disposing the DockingControler, when its last DockablePanel is closed. This is done in the following.

#region Delegates (DockingControler)
private void dockPanel_PrepareClosing(DockablePanel dockPanel)
{
   DockablePanel remaining;
   switch (dockPanel.DockingType)
   {
      case DockType.Left:
      case DockType.Right:
      case DockType.Top:
      case DockType.Bottom:
         _dockedPanelsCount = 0;
         break;
      case DockType.Upper:
      case DockType.LeftSide:
         // which Panel will be the remaining one
         remaining =
            (DockablePanel)DockSplitContainer.Panel2.Controls[0];
         // we remove the DockablePanel in the first place
         // of the SplitContainer
         DockSplitContainer.Panel1.Controls.Remove(dockPanel);
         // change position from panel2 to panel1 - remove it at
         // the second place of the SplitContainer
         DockSplitContainer.Panel2.Controls.Remove(remaining);
         // we need to reesteblish the condition of one
         // DockablePanel; therefore, we collapse the second
         // SplitConainer space
         DockSplitContainer.Panel2Collapsed = true; 
         // And add the remaining panel in the first slot of the
         // SplitContainer
         DockSplitContainer.Panel1.Controls.Add(remaining);
         // Get the correct Docktype for the remaining Dockablepanel
         remaining.DockingType = GetDockType(this.Dock);
         // Correct the counter
         _dockedPanelsCount = 1;
         break;
      case DockType.Lower:
      case DockType.RightSide:
         DockSplitContainer.Panel2.Controls.Remove(dockPanel);
         remaining =
            (DockablePanel)DockSplitContainer.Panel1.Controls[0];
         DockSplitContainer.Panel2Collapsed = true; 
         remaining.DockingType = GetDockType(this.Dock);
         _dockedPanelsCount = 1;
         break;
      case DockType.Center:
         // will be added later in this article
         break;
      default:
         break;
   }
   RemoveDelegates(dockPanel);
   if (_dockedPanelsCount == 0)
   {
      this.Visible = false;
      // this controler no longer holds panels so it is 
      //going to be disposed and removed from the collection
      this._dockingManager.Admin.RemoveDockControler(this._key);
      this.Dispose();
   }
}
#endregion

I have added a full description of the different steps of the procedure in the case when you close a DockablePanel that was docked to Upper or LeftSide of a specific DockingControler directly into the above code. Here is the explanation for closing a DockablePanel that was docked to Lower or RightSide. In this case, the remaining DockablePanel is already in the wanted first slot of the SplitContainer so you do not need to change its position. You need only to remove the closing panel, to collapse the slot where it had been, and at last to reset remaining panels DockingType property, because when you remove one DockablePanel, there is only one panel left and this panel now fills the full SplitContainer so for the user it looks to be docked in the same way the DockingControler is docked. Therefore, you get the new DockType for the remaining DockablePanel by reading how the DockingControler is docked. You can find this by reading its Dock property but this is of type DockStyle. Because of this, you need to convert it from DockStyle to DockType (as shown in the next code segment). By comparing these two enums, you will find that they are equal in the lower values up to the fill value.

#region private Methods (DockingControler)
private DockType GetDockType(DockStyle style) {
   if (style <= DockStyle.Fill) {
      return (DockType)style;
   }
   else {
      return DockType.None;
   }
}
#endregion

Trying to compile, you will see this is working now. So, go on to finish the advanced docking methods that are done using the SplitControler in adding the code needed for undocking them without closing the DockablePanel.

Creating a DockablePanel-Controlmanager Using C#, Part 6

Undocking: Leaving the SplitContainer

Similarly to what you did earlier in the closing process, you also need to make sure that removing one of the panels doesn't keep the controller in an undefined condition. Removing one panel always needs to have the remaining panel (if there still is one) in slot1 of the SplitContainer; slot2 needs to be collapsed. It's really very similar to the previous code, you will see. So at first, you need to add a variable for the remaining DockablePanel. You will do this in the first line of your method, so it can be used anywhere in this method.

#region Delegates (DockingControler)
private void dockPanel_Undock(DockablePanel dockPanel) {
   DockablePanel remaining = null;
   // we already have the following lines
   this._dockingManager.Admin.RemoveDockedPanel(dockPanel.Key);
   dockPanel.Visible = false;
   dockPanel.Dock = DockStyle.None;
   switch (dockPanel.DockingType) {
      case DockType.Left:
      case DockType.Right:
      case DockType.Top:
      case DockType.Bottom:
         if (this.DockSplitContainer.Panel1.Controls.
             Contains(dockPanel)) {
            this.DockSplitContainer.Panel1.Controls.
               Remove(dockPanel);
            _dockedPanelsCount = 0;
         }
         break;
      // here we go with the new added code
      case DockType.Upper:
      case DockType.LeftSide:
         if (this.DockSplitContainer.Panel1.Controls.
            Contains(dockPanel)) {
            remaining = (DockablePanel)this.DockSplitContainer.
               Panel2.Controls[0];
            // Remove the DockablePanel to be undocked
            this.DockSplitContainer.Panel1.Controls.
               Remove(dockPanel);
            // Change position of the remaining panel-
            // add remaining in Panel1
            // but remove it in panel 2 before
            this.DockSplitContainer.Panel2.Controls.Remove(remaining);
            this.DockSplitContainer.Panel1.Controls.Add(remaining);
            // collapse panel2
            this.DockSplitContainer.Panel2Collapsed = true;
            // the docking Type is now what the panel itself
            // is docked
            remaining.DockingType = GetDockType(this.Dock);
            // now we have the standard padding of a single panel
            // ( no bar for changeing sizerelation)
            this.DockSplitContainer.Panel1.Padding =
               new Padding(0, 0, 0, 0);
            _dockedPanelsCount = 1;
         }
         break;
      case DockType.Lower:
      case DockType.RightSide:
         if (this.DockSplitContainer.Panel2.Controls.
            Contains(dockPanel)) {
            this.DockSplitContainer.Panel2.Controls.Remove(dockPanel);
            // panel 1 stays still there 
            remaining = (DockablePanel)this.DockSplitContainer.
               Panel1.Controls[0];
            // but its docking Type changed
            this.DockSplitContainer.Panel2Collapsed = true;
            remaining.DockingType = GetDockType(this.Dock);
            this.DockSplitContainer.Panel1.Padding =
               new Padding(0, 0, 0, 0);
            _dockedPanelsCount = 1;
         }
         break;
      case DockType.Center:
         // we do this codpart a bit later
         break;
      default:
         return;
   }
   // remove delegate so it doesn't react 
   RemoveDelegates(dockPanel);
   if (_dockedPanelsCount == 0) {
      this.Visible = false;
      // this Controler is closed and disposed so we can remove it
      // from the collection
      this._dockingManager.Admin.RemoveDockControler(this._key);
      this.Dispose();
   }
}
//. . .
#endregion

Now you have done all that's required. You may compile and test it. You will be able to add and remove or delete DockablePanels just as you want it.

Unwanted Side Effects

For testing reasons, you want to change the Caption of your TechForm in the test application so you would be able to differentiate between the panels you are adding. Therefore, in the MDIForm you add a field to count and in the last line of your tsbAddTechList_Click delegate you add the following:

namespace DockingControlTestApp{
   public partial class MDIForm : Form {
      private int count = 1;
      //. . .

#region delegates (MDIForm - DockingControlTestApp)
private void tsbAddTechList_Click(object sender, EventArgs e) {
      //. . .
      frmTechList.Text += " " + count.ToString();
      count++;
}
//. . .
#endregion

This should add a number to each Caption, so you want to get 'List of Technicians 1', List of Technicians 2', and so on. Try it, but you will see nothing happens. The DockableForm obviously has accepted the Caption you set at design time, but didn't accept to be set at runtime? Yes, that comes from the fact you are cheating. Remember, on the screen you permanently see the DockablePanel; the DockableForm is hidden. This is a side effect of the given design; it needs to be solved. There are other side effects too, but you'll handle them one by one, when you need to handle them to go on with your design. Now, let's add a new Text Property in the DockableForm to handle this.

#region Properties (DokableForms)
public new string Text {
   get {
      if (_dockPanel != null) {
         return _dockPanel.Caption;
      }
      return base.Text;
   }
   set {
      if (_dockPanel != null) {
         _dockPanel.Caption = value;
      }
      base.Text = value;
   }
}
//. . .
#endregion

Now, you will get the following result.

[AdvancedDockingSecondResult.JPG]

Figure 8: Captions can be set and changed at runtime.

The common solution for most of the side effects is to add all changes that could be done on your Form to your DockablePanel too. Events fired from the DockablePanel need their corresponding Event in the DockableForm and vice versa.

To just give another example,you want to be able to use the Closing event of the DockableForm to allow the user to cancel closing the panel. In the MDIForm of your DockingControlTestApp, you add the frm_FormClosing delegate just when you create the TechForm. In the last line of the tsbAddTechList_Click delegate, you add the new closing delegate to your TechForm that is inherited from DockableForm. So, you now have the following code.

#region delegates (MDIForm - DockingControlTestApp)
private void tsbAddTechList_Click(object sender, EventArgs e) {
   Point where = new Point(150, 150);
   // Create a new TechForm object 
   TechForm frmTechList = new TechForm();
   CreateDockableForm(where, frmTechList);
   frmTechList.Text += " " + count.ToString();
   count++;
   frmTechList.FormClosing +=
      new FormClosingEventHandler(frmTechList_FormClosing);
}
// and the delegate
void frmTechList_FormClosing(object sender, FormClosingEventArgs e) {
   if (MessageBox.Show("Should the TechForm be closed ",
      "Close TechForm", MessageBoxButtons.YesNo) ==
         DialogResult.No) {
      e.Cancel = true;
   }
}
#endregion

Or, if you prefer to override the OnFormClosing() method, do the following. Both are possible. In the added example, you use this. But, make sure you can do only one of these two methods.

#region protected overrides (TechForm)
protected override void OnFormClosing(FormClosingEventArgs e) {
   e.Cancel = (MessageBox.Show("Do you really want to close this Form",
   "ClosingRequest",MessageBoxButtons.YesNo,MessageBoxIcon.Question)
       == DialogResult.No);
   base.OnFormClosing(e);
}
#endregion

Now, you have to make sure that your program works independently in which way you are implementing that OnFormClosing() works. On FormClosing is called when the Form is caused to be closed. So, you cannot remove the DockablePanel after this all is done to delete the DockableForm too because, if the user decides to cancel closing, the dockablePanel already is destroyed. This means that closing the _carrier form, which is the DockableForm, needs to be the first action in the cycle when the closing action is induced by the user clicking the cross in the captionbar.

Creating a DockablePanel-Controlmanager Using C#, Part 6

Some changes are needed to get the order of commands changed. In the Close() method of your DockablePanel, therefore, you add a few lines. You are not sure whether the user will realy close the DockableForm; this will depend on if he answers the messagebox question with yes or no. If he really closes the Form, it needs to be removed in the DockingManagers AllCarriers collection. So, you remove it just before you cause the _carrier Form to close. If the action is done, IsDisposed of the Form will be true, so you no longer have a carrier attached and you are going to fire ClosingPanel by using the OnClosingPanel() method just as you did before and you dispose your DockablePanel. If the user cancels the close command, the carrier again is attached to the AllCarriers collection in the DockingManager. Here is the code:

#region internal Methods (DockablePanel)
internal void Close() {
   _dockingManager.RemoveCarrier(_carrierForm.Key );
   _carrierForm.Close();
   if (_carrierForm != null && !_carrierForm.IsDisposed) {
      _dockingManager.AddCarrier(_carrierForm);
   }else{
      _carrierAttached = false;
      OnClosingPanel();
      if (!this.IsDisposed) {
         this.Dispose();
      }
   }
}
#endregion

To be able to add or remove the carrier, you need to add two simple methods to the DockingManager.

#region internal methods (DockingManager)
internal void RemoveCarrier(string key) {
   if( AllCarriers.ContainsKey(key)){ 
      AllCarriers.Remove(key);
   }
}

internal void AddCarrier(DockableForm _carrierForm) {
   if (!AllCarriers.ContainsValue( _carrierForm)){
      AllCarriers.Add(_carrierForm.Key, _carrierForm);
   }
}
#endregion

The dockPanel _PanelClosing delegate needs to be reduced now, because the former lines where you removed the carrier in the AllCarriers collection are no longer needed here, because this is done just before in the Close() method of the DockablePanel. And the carrier was already closed 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);
   }
   // Its closed so we remove it from AllDockPanels too
   AllDockPanels.Remove(key);
   // the carrier is already removed, we dont need the
   // former code here anymore
}
#endregion

In the resize method of the Dockablepanel, you make sure that the carrier isn't already disposed when you want to resize it.

private void DockablePanel_Resize(object sender, EventArgs e) {
//. . .
   if (_dockingType != DockType.None && _carrierForm != null &&
      !_carrierForm.Disposing ){
      // if we are docked we are not attached to the carrier so
      // we need to size the carrierForm if we have closed the
      // DockableForm we dont resize it even when before also
      // closing the DockablePanel a Sizing action may occur
      _carrierForm.Size = new Size(this.Width, this.Height);
   }
}

Now, you have corrected the unwanted side effects the design has shown. You will need to do this from time to time during the design increases.

Creating Advanced Docking DockType.Center

You have really done a lot of work throughout this series, so now it's time to take the bull by the horns!

As you know by now, the first DockablePanel is always docked, by adding it to the SplitContainer. Therefore, docking to a DockingControler using DockType.Center needs to change the control you are using to add your DockablePanels. This may happen when adding the second DockablePanel, but this also may occur when you try to add the third DockablePanel. Additionally, you need to have an eye on what happens when removing a panel from the TabControl. As long as there are more than two DockablePanels added to the TabControl, you are simply removing the panels. When you have reached the point where only two DockablePanels are left, this changes.

Removing one DockablePanel then also removes the TabControl and adds the remaining panel to the SplitContainer again where the second slot is collapsed. This is necessary because a DockingControler with only one DockablePanel always needs to be the same, independent if you have just added your first panel to the DockingControler or if you had a DockingControler that contained a lot of panels in a TabControl, and you have removed all of them, so that there is only one panel left. The result always needs to be the same. If you have only one DockablePanel added to a DockingControler, it needs to be added into the first slot of a SplitContainer; slot 2 is collapsed. That's all. Now, go on to code this.

First, you have to add the TabControler field to your private fields so you can access it from everywhere in the DockingControler.

#region Fields (DockingControler)
private TabControl _tabControler;
private const int REG_HEIGHT = 22;    // Height of a register card

The first time you add a DockablePanel to a DockingControler using DockType.Center, the ControlerType of the DockingControler is ControlerType.Vertical or ControlerType.Horizontal. When you already have a panel docked using Docktype.Center, the ControlerType is changed to ControlerType.Tabbed. You use this method for identification when you have to create the _tabControler and when you have only to create a new page, to add the DockablePanel to it and to add the TabPage itself then to the _tabControler. Therefore, in the switch statement of the AddPanel method where you already have prepared the space for your code, you add the following code.

#region internal Methods(DockingControler)
internal void AddPanel(DockablePanel dockPanel) {
//. . .
case DockType.Center:
   if (_controlsType != ControlerType.Tabbed) {
      CreateTabbedDockControler(dockPanel);
      _controlsType = ControlerType.Tabbed;
   } else {
      AddNewTabPage(dockPanel);
      _controlsType = ControlerType.Tabbed;
   }
   _dockedPanelsCount = _tabControler.TabCount;
   break;
#endregion

When creating the _tabControler, at first you are suspending the layout for the whole action. The Tab pages are to be aligned to the bottom and the Controler itself is docked to the DockingControler using DockStyle.Fill. Then, you need to find out where the tabControler's parent, the DockingControler, is docked. Depending on that, the Padding of the DockingControler is adjusted and the Size of the tabControler too. Then, you check whether you already have one or two DockablePanels docked to the DockSplitContainer; removd them there and add them to a TabPage and to the _tabControler using the AddNewTabPage method. After you have added the already existing DockablePanels, you only need to add just that panel that you are just trying to dock. The empty DockSplitContainer is disposed, the _tbControler is added to the Controls collection of our DockingControler, and you are finished. That's what you will find line by line in the following code.

#region private Methods (DockingControler)
   private void CreateTabbedDockControler(DockablePanel dockPanel) {
      _tabControler = new TabControl();
      _tabControler.SuspendLayout();
      this.SuspendLayout();
      _tabControler.Alignment = TabAlignment.Bottom;
      _tabControler.Dock = DockStyle.Fill;
      _tabControler.Name = "TabControler";
      _tabControler.SelectedIndex = 0;
      switch (this.Dock) {
         case DockStyle.Top:
            _tabControler.Location = new Point(0, 0);
            this.Padding = new Padding(0, 0, 0, PAD_VER);
            _tabControler.Size = new Size(this.Width,
               this.Height - PAD_VER);
            break;
         case DockStyle.Bottom:
            _tabControler.Location = new Point(0, PAD_VER);
            this.Padding = new Padding(0, PAD_VER, 0, 0);
            _tabControler.Size = new Size(this.Width,
               this.Height - PAD_VER);
            break;
         case DockStyle.Left:
            _tabControler.Location = new Point(0, 0);
            this.Padding = new Padding(0, 0, PAD_HOR, 0);
            _tabControler.Size = new Size(this.Width - PAD_HOR,
               this.Height);
            break;
         case DockStyle.Right:
            _tabControler.Location = new Point(PAD_HOR, 0);
            this.Padding = new Padding(PAD_HOR, 0, 0, 0);
            _tabControler.Size = new Size(this.Width - PAD_HOR,
               this.Height);
            break;
      }
      DockablePanel panel1 =
         (DockablePanel)DockSplitContainer.Panel1.Controls[0];
      AddNewTabPage(panel1);
      DockSplitContainer.Panel1.Controls.Clear();
      if (_dockedPanelsCount == 2) {
         DockablePanel panel2 =
            (DockablePanel)DockSplitContainer.Panel2.Controls[0];
         AddNewTabPage(panel2);
         DockSplitContainer.Panel2.Controls.Clear();
      }
      AddNewTabPage(dockPanel);
      DockSplitContainer.Visible = false;
      DockSplitContainer.Dispose();
      this.Controls.Add(this._tabControler);
      _tabControler.ResumeLayout(false);
      this.ResumeLayout(false);
   }

The AddNewTabPage() method creates a new TabPage object and add the Dockablepnel to its Controls collection. You store the DockType in the DockablePanels' DockingType property for later use. Also, the Name of theTabPage is set to the DockablePanel key. This is done for being able to have quicker access to a specific DockableControl that is added to one of the TabControlers Pages by simply checking the TabPages' names instead of running trough all Controls collections of every TabPage when searching for a specific DockablePanel. The TabPages Text is set to the caption of the DockablePanel so you can see in the Tab control which panel is docked.

#region private Methods (DockingControler)
private void AddNewTabPage(DockablePanel dockPanel) {
   TabPage newPage = new TabPage();
   newPage.Controls.Add(dockPanel);
   dockPanel.DockingType = DockType.Center;
   newPage.Location = new Point(0, 0);
   newPage.Name = dockPanel.Key;
   newPage.Padding = new Padding(0);
   newPage.Size = new Size(_tabControler.Width,
                           _tabControler.Height - REG_HEIGHT);
   newPage.Text = dockPanel.Caption;
   newPage.UseVisualStyleBackColor = true;
   _tabControler.Controls.Add(newPage);
}

Closing a DockablePanel docked using DockType.Center

Now, you are able to dock your DockablePanel to the DockingControler by adding it to a TabControl. There are two actions missing in these mechanics. One is a method to close the DockablePanel when it is docked this way; the other is to add code for removing the DockablePanel from its TabControl again when undocking a Panel from its DockingControler. You already have the code for all DockTyes; only DockType Center is missing. Both demands need to remove the DockablePanel from the TabControl. Therefore, you are able to use the same code in this segment of code. So, you use the RemovePanelFromTabControler() method to get this done and into the dockPanel_PrepareClosing as well as into the dockPanel_Undock delegate you insert this method in the relevant codepart.

#region delegates (DockingControler)
private void dockPanel_PrepareClosing(DockablePanel dockPanel){
//. . .
   case DockType.Center:
      RemovePanelFromTabControler(dockPanel);
      break;
 //. . .
}

private void dockPanel_Undock(DockablePanel dockPanel) {
//. . .
   case DockType.Center:
      RemovePanelFromTabControler(dockPanel);
      break;
 //. . .

#endregion

Let me discuss the code for preparing to close or remove a DockablePanel. You are looping through the TabControl, looking for the page that is to be closed and, when found, you remove the TabPage, so it is no longer part of the TabControl. Then, you remove the DockablePanel from the TabPage. This way, the TabPage is without any connection so it will be disposed automatically. If there is only one TabPage remaining in the TabControl, you need to change back from a TabControl to a SplitContainer to contain the DockablePanel. (I have described these mechanics already).

#region private Methods (DockingControler)
private void RemovePanelFromTabControler(DockablePanel dockPanel) {
   for (int i = 0; i < _tabControler.TabPages.Count; i++) {
      if (_tabControler.TabPages[i].Controls.Contains(dockPanel)) {
         TabPage tabToRemove = _tabControler.TabPages[i];
         _tabControler.TabPages.Remove(tabToRemove);
         // remove from TabPage
         if (tabToRemove.Controls.Contains(dockPanel)) {
            tabToRemove.Controls.Remove(dockPanel);
         }
         break;
      }
   }
   _dockedPanelsCount = _tabControler.TabPages.Count;
   // if we have only one we untab the control so we need
   // the panel itself
   if (_dockedPanelsCount == 1) {
        ChangeTabControlerToMultiDockControler();
   }
}
#endregion

In the following method, you simply remove the last remaining DockablePanel from the TabPage, correct the Docktype of the remaining DockablePanel, getting the correct new ControlerType of your DockingControler, and then dispose the TabControl and create the DocksplitContainer by usage of the already existing CreateMultiDockControler() method.

#region private methods (DockingControler)
private void ChangeTabControlerToMultiDockControler() {
   DockablePanel remaining;
   this.Visible = false;
   // we get the remaining panel
   if (_tabControler.TabPages.Count > 0) {
      TabPage tabPage = _tabControler.TabPages[0];
      remaining = (DockablePanel)tabPage.Controls[0];
      // which way the controler is docked
      DockStyle ctlStyle = this.Dock;
      ControlerType ctlType;
      if (ctlStyle == DockStyle.Left || ctlStyle ==
         DockStyle.Right) {
         ctlType = ControlerType.Vertical;
      }
      else {
         ctlType = ControlerType.Horizontal;
      }
      // now we delete the _tabControler
      _tabControler.TabPages[0].Controls.Clear();
      _tabControler.TabPages.Clear();
      this.Controls.Clear();
      _tabControler.Visible = false;
      _tabControler.Dispose();
      // we add it to a normal DockSplitControler
      _controlsType = ctlType;
      remaining.DockingType = GetDockType(this.Dock);
      // correct the DockType of the remaining DockablePanl
      CreateMultiDockControler(ctlType, remaining);
      this.Visible = true;
   }
}

Here is the compiled result.

[ResultAdvancedReady.JPG]

Figure 9: The DockingManager showing different Docking methods.

Conclusion

You have finished the Advanced docking methods now. The next articles will contain how to pin and unpin this panel and how to use hooking the mouse to open and close the DockingControler when hovering over a buttonStrip. I hope you enjoy these lessons and will be happy to meet you in the next article.



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

  • Today's agile organizations pose operations teams with a tremendous challenge: to deploy new releases to production immediately after development and testing is completed. To ensure that applications are deployed successfully, an automatic and transparent process is required. We refer to this process as Zero Touch Deployment™. This white paper reviews two approaches to Zero Touch Deployment--a script-based solution and a release automation platform. The article discusses how each can solve the key …

  • On-demand Event Event Date: October 29, 2014 It's well understood how critical version control is for code. However, its importance to DevOps isn't always recognized. The 2014 DevOps Survey of Practice shows that one of the key predictors of DevOps success is putting all production environment artifacts into version control. In this webcast, Gene Kim discusses these survey findings and shares woeful tales of artifact management gone wrong! Gene also shares examples of how high-performing DevOps …

Most Popular Programming Stories

More for Developers

RSS Feeds