Introduction
Have you ever been faced with the task of displaying multiple pages on the same Windows Form? Most of us would certainly respond in the affirmative, and most of us have addressed this by using the good-old Tab Control. While tabs are undoubtedly a proven way of handling such cases, there are situations calling for a "less generic" approach. What if we need to make our form a bit more visually appealing and use icons or graphical buttons to flip through pages? What if we don't want to display tabs at all? Many familiar applications feature such graphical interfaces (see illustration); however, the .NET Framework offers no built-in tools to accomplish this, at least at the time of this writing.
For my purposes, I had to build a control from scratch, thanks to the framework's extensible nature. When the project was completed, I felt that the experience was worth sharing with the developer community, so here we go: a custom Multi-Page Control.
Please note that the implementation of a particular page-flipping mechanism (such as icons or list-box items) is beyond the scope of this article. Rather, it focuses on a custom Windows Forms control that can host multiple pages of child controls, as well as the programming model for using the control in a Windows Forms project. For simplicity, I am using standard Windows Forms controls - buttons and combo-box items - for page activation.
Before we plunge into the code, a few words on the level of training recommended for better understanding of the subject. Familiarity with C# and object oriented programming is expected, with some experience in Windows Forms programming. Knowledge of some of the .NET Framework's advanced features, such as reflection, is helpful, although not 100% required.
You will also notice that I do not use Hungarian notation in my code; however, reading the code should not be a problem, since the notation is, in fact, quite simple and easy to understand. The prefixes are assigned as such:
a
: local variables
the
: method's formal parameters, as well as parameters passed to exception-handling catch
blocks
my
: instance fields of a class
our
: static fields of a class
Properties and methods are named using the Camel case notation (each word is capitalized):
DoActionOne();
PropertyTwo = 1;
Step 1. Creating the Control
Our MultiPaneControl
is a .NET class that inherits from System.Windows.Forms.Control
. Individual pages are instances of the MultiPanePage
class. Since the MultiPanePage
class is all about containing other controls, it makes perfect sense to derive it from System.Windows.Forms.Panel
:
public class MultiPaneControl : System.Windows.Forms.Control
{
}
public class MultiPanePage : System.Windows.Forms.Panel
{
}
Adding a page to our control is a no brainer:
Controls.Add( new MultiPanePage() );
Now, to set the current page, we will add a dedicated property and toggle visibility within its set
block:
protected MultiPanePage mySelectedPage;
public MultiPanePage SelectedPage
{
get
{ return mySelectedPage; }
set
{
if (mySelectedPage != null)
mySelectedPage.Visible = false;
mySelectedPage = value;
if (mySelectedPage != null)
mySelectedPage.Visible = true;
}
}
Since all members of the MultiPaneControl
's Controls
collection (i.e., all pages) should share such basic characteristics as positioning and pixel dimensions, it's a good idea to set those from within the parent control. Our control must also support background transparency. And we want to make sure that no other class instance, except MultiPanePage
, can be added to the MultiPaneControl
, so we will secure the addition with a conditional statement:
public MultiPaneControl()
{
ControlAdded += new ControlEventHandler(Handler_ControlAdded);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
BackColor = Color.Transparent;
}
private void Handler_ControlAdded(object theSender, ControlEventArgs theArgs)
{
if (theArgs.Control is MultiPanePage)
{
MultiPanePage aPg = (MultiPanePage) theArgs.Control;
aPg.Location = new Point(0, 0);
aPg.Dock = DockStyle.Fill;
if (Controls.Count == 1)
mySelectedPage = aPg;
else
theArgs.Control.Visible = false;
}
else
{
Controls.Remove(theArgs.Control);
}
}
One final thing for us to do is override the DefaultSize
property:
protected static readonly Size ourDefaultSize = new Size(200, 100);
protected override Size DefaultSize
{
get { return ourDefaultSize; }
}
At this point, our control is ready to be compiled and tested in the code.
Compiling the Step1 Sample Project yields a control that works fine at runtime; however, dealing with it in the design environment reveals an extensive list of shortcomings:
- There is no way to create an instance of
MultiPaneControl
on a form except by changing its source code.
- Adding and removing pages is also not possible without altering the source code.
- Pages can still be moved and sized individually: even though we've initially set the
Dock
property to prevent independent dragging/sizing, it can be easily altered through the Properties window.
- The only way to edit a page graphically is by setting the control's
SelectedPage
property so that the page becomes visible.
- When editing the
SelectedPage
property through the Properties window, Visual Studio displays all objects of type MultiPanePage
, including those belonging to other instances of MultiPaneControl
. In reality, only children of the control in question should be accessible.
Inadequacies of this sort are quite common in custom controls that lack Design-Time support, so our next step will be precisely that: enabling our control to work smoothly with Visual Studio's RAD tools.
Step 2. Adding Design-Time Support: the Basics
Toolbox Item
In this step, we will develop a Toolbox item for our MultiPaneControl
. Since the Toolbox item is not an independent entity but rather an extension to the MultiPaneControl
, they will be compiled together into a standalone assembly. This will allow us to utilize our control in any Windows Forms project we may lay our hands on in the future. To uniquely identify our control on the Toolbox, we will also design a custom icon for it.
When in action, the Toolbox item will create an instance of our control in response to a drag-and-drop - or "drawing" event. By default, the new control will contain one page. To associate a Toolbox item with a control, we need to apply the ToolboxItem
attribute to the control's class:
[ToolboxItem(typeof(MultiPaneControlToolboxItem))]
public class MultiPaneControl : Control
{
...
}
The code above instructs Visual Studio to create an instance of MultiPaneControlToolboxItem
which is then used to instantiate our MultiPaneControl
. The former class must inherit from System.Drawing.Design.ToolboxItem
. It must also be declared as Serializable
since Visual Studio will serialize it into its internal data structures. Because System.Drawing.Design.ToolboxItem
implements the ISerializable
interface, we also need to define a special serialization constructor as is stated in the Microsoft documentation.
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
[Serializable]
public class MultiPaneControlToolboxItem : ToolboxItem
{
public MultiPaneControlToolboxItem() : base(typeof(MultiPaneControl))
{
}
public MultiPaneControlToolboxItem(SerializationInfo theInfo,
StreamingContext theContext)
{
Deserialize(theInfo, theContext);
}
protected override IComponent[] CreateComponentsCore(IDesignerHost theHost)
{
MultiPaneControl aCtl =
(MultiPaneControl)theHost.CreateComponent(typeof(MultiPaneControl));
MultiPanePage aNewPage1 =
(MultiPanePage)theHost.CreateComponent(typeof(MultiPanePage));
aCtl.Controls.Add(aNewPage1);
return new IComponent[] { aCtl };
}
}
As we can see, it is the CreateComponentsCore
method that is responsible for creating our control and supplying it with a page. Visual Studio does the rest for us automatically by adding the appropriate code to the form.
Let's take some time away from coding and put our artistic hat on, since it's time to design an icon for our control! This icon will be displayed in the Toolbox (all versions of Visual Studio) and in the Document Outline window (VS 2005 or higher).
- In your preferred graphics editor, draw a 16x16-pixel image.
- Save it as a bitmap image, naming the file [Your control's class name].bmp. In our case, the images for
MultiPaneControl
and MultiPanePage
controls will be named MultiPaneControl.bmp and MultiPanePage.bmp, respectively.
- Using the Project -> Add Existing Item... menu, place the images in the root of your project, i.e., the folder where your *.cs files reside.
- In the Properties window for each bitmap, set the Build Action property to Embedded Resource.
- Make sure that your control classes reside in the project default namespace as shown below:
To preclude independent creation of MultiPanePage
instances, we apply the following attribute to the class:
[ToolboxItem(false)]
public class MultiPanePage : Panel
{
...
}
UI Type Editor
Next, we are going to implement an editor for the SelectedPage
property. As I've mentioned before, when we edit this property via the Properties Window, Visual Studio filters the list of items based solely on the item type. Suppose our form contains two instances of MultiPaneControl
: Control A and Control B, each with its own collection of pages. Setting a page that belongs to Control A as the selected page of Control B is undesirable and error-prone, aside from the fact that it plain doesn't make any sense. The list of available objects should be restricted to pages that are part of the control being edited.
The illustration below shows a form with two MultiPaneControl
s, the upper control containing three pages (myCtlPanePage1
, myCtlPanePage2
, myCtlPanePage3
), and the lower, two pages (myCtlPanePage4
and myCtlPanePage5
). The lower control is currently selected. The UI Type Editor reflects the fact that the pages belong to two different controls and only displays the appropriate objects.
To accomplish this, we need to create a class that derives, either directly or indirectly, from System.ComponentModel.Design.UITypeEditor
. Our editor will inherit from System.ComponentModel.Design.ObjectSelectorEditor
, which is ideal for the problem we are trying to address. This class displays a drop-down list of items in response to an arrow click. Our derived class, in turn, will only be responsible for populating the list.
internal class MultiPaneControlSelectedPageEditor : ObjectSelectorEditor
{
protected override void FillTreeWithData(Selector theSel,
ITypeDescriptorContext theCtx, IServiceProvider theProvider)
{
base.FillTreeWithData(theSel, theCtx, theProvider);
MultiPaneControl aCtl = (MultiPaneControl) theCtx.Instance;
foreach (MultiPanePage aIt in aCtl.Controls)
{
SelectorNode aNd = new SelectorNode(aIt.Name, aIt);
theSel.Nodes.Add(aNd);
if (aIt == aCtl.SelectedPage)
theSel.SelectedNode = aNd;
}
}
}
With this class in place, all we need to do is apply the Editor
attribute to the property:
[
Editor(
typeof(MultiPaneControlSelectedPageEditor),
typeof(System.Drawing.Design.UITypeEditor) )
]
public MultiPanePage SelectedPage
{
...
}
We are almost ready to compile our Step2 sample project. To wrap things up, we'll hide the two MultiPanePage
's properties that are not meant for direct editing, namely Dock
and Location
:
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override DockStyle Dock
{
get { return base.Dock; }
set { base.Dock = value; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Point Location
{
get { return base.Location; }
set { base.Location = value; }
}
Please note that we must not apply the same technique to the Size
property, as Visual Studio uses it for laying out child controls.
Step 3. Adding Design-Time Support. Control Designers.
So far, we have covered only three of the five design-time issues outlined earlier. As a result:
MultiPaneControl
instances can now be created graphically from Visual Studio's Toolbox.
- Pages can no longer be dragged independently of their container control.
- The
SelectedPage
property has an error-proof UI Type Editor.
Addressing the remaining problems involves creation of control designers for both the MultiPaneControl
and MultiPanePage
controls. In this section, we will be dealing with two components, the Control and the Designer. It's important not to confuse methods and properties of the Designer with those of the Control.
The steps to provide design-time support to the MultiPaneControl
are:
- Create a
MultiPaneControlDesigner
that implements the IDesigner
interface, as outlined in the Microsoft documentation.
- Associate our Control with the Designer and make Visual Studio aware of this association. This is achieved by applying the
Designer
attribute to the MultiPaneControl
class.
We can derive our Designer class from several classes featured by the .NET Framework that implement the IDesigner
interface. These are:
ControlDesigner
- default designer class used with all controls that don't have a custom designer
ParentControlDesigner
- inherits full functionality of ControlDesigner
and supports dragging controls onto the edited control's surface
ScrollableControlDesigner
- inherits full functionality of ParentControlDesigner
and supports processing of scrolling events at design time
Being that the sole purpose of the MultiPaneControl
class is hosting other controls, ParentControlDesigner
seems to be the best choice for our designer.
MultiPaneControlDesigner
The key in developing a control's designer is its interaction with the development environment. Such interaction relies on the so called designer services, i.e., objects obtained as a result of calling the GetService
method. Each object implements an interface that serves its own specific purpose. For instance, the ISelectionService
is used to programmatically select/deselect components on a form.
A reference to the interface must be obtained in order to access its functionality. In other words, we are not guaranteed that our development environment (or, rather, its version) will support a certain interface. For example, a control designer developed for Visual Studio 2008 may crash an older version of VS if the latter lacks support for the necessary interface. To determine whether such support is present, we need to explicitly check the value returned by the GetService
method:
void MyFunc(ComponentDesigner theDesigner)
{
Type aType = typeof(ISelectionService);
ISelectionService aSrv = (ISelectionService) theDesigner.GetService(aType);
if (aSrv != null)
{
}
}
If you have experience in COM programming, you will notice a strong resemblance here in the way references are obtained, as well as how we check to see if the interface is actually supported. In contrast to COM, though, our reference does not need to be released in the .NET environment.
So, we will set off by establishing contact between the IDE and our control designer. To do so, we will override the Initialize
method and subscribe to several events provided by designer services. We'll also initialize the Designer Verbs from within this method.
A Designer Verb may be viewed as a single action or command. Each designer has verbs unique to the task for which it was built. Visual Studio displays verbs in the control's context menu. Versions 2005 and higher also show the verbs in a popup window activated by clicking on the small triangle in the top left corner of the control's bounding area:
When the verb is called by the user, an event is raised and the system automatically invokes its handler. In the handler, the designer performs verb-specific actions, perhaps modifying the properties of the control. Now, let's look at the Initialize
method:
public override void Initialize(IComponent theComponent)
{
base.Initialize(theComponent);
ISelectionService aSrv_Sel =
(ISelectionService)GetService(typeof(ISelectionService));
if (aSrv_Sel != null)
aSrv_Sel.SelectionChanged += new EventHandler(Handler_SelectionChanged);
IComponentChangeService aSrv_CH =
(IComponentChangeService)GetService(typeof(IComponentChangeService));
if (aSrv_CH != null)
{
aSrv_CH.ComponentRemoving += new ComponentEventHandler(Handler_ComponentRemoving);
aSrv_CH.ComponentChanged +=
new ComponentChangedEventHandler(Handler_ComponentChanged);
}
myAddVerb = new DesignerVerb("Add page", new EventHandler(Handler_AddPage));
myRemoveVerb = new DesignerVerb("Remove page", new EventHandler(Handler_RemovePage));
mySwitchVerb =
new DesignerVerb("Switch pages...", new EventHandler(Handler_SwitchPage));
myVerbs = new DesignerVerbCollection();
myVerbs.AddRange(new DesignerVerb[] { myAddVerb, myRemoveVerb, mySwitchVerb });
}
We have subscribed to three events here, so we'll have to do exactly the opposite in the Dispose
method override:
protected override void Dispose(bool theDisposing)
{
if (theDisposing)
{
ISelectionService aSrv_Sel =
(ISelectionService)GetService(typeof(ISelectionService));
if (aSrv_Sel != null)
aSrv_Sel.SelectionChanged -= new EventHandler(Handler_SelectionChanged);
IComponentChangeService aSrv_CH =
(IComponentChangeService)GetService(typeof(IComponentChangeService));
if (aSrv_CH != null)
{
aSrv_CH.ComponentRemoving -=
new ComponentEventHandler(Handler_ComponentRemoving);
aSrv_CH.ComponentChanged -=
new ComponentChangedEventHandler(Handler_ComponentChanged);
}
}
base.Dispose(theDisposing);
}
Let's take a look at the event handlers. Since only one page can be visible at a time, selecting a child control that is located on a hidden page must set this page to be visible. The handler for the SelectionChanged
event will be used to monitor control selection and toggle visibility.
The handler for ComponentRemoving
is required to update the SelectedPage
property, if necessary. This way the property will always be in sync with the Controls
collection.
Finally, the handler for ComponentChanged
is useful for enabling/disabling particular verbs. For example, we won't allow page deletion for a control that has only one page.
Our designer will override a few more methods. For example, we want to prevent anything other than MultiPanePage
instances from being added as child controls (the same concept we had to implement for the control itself):
public override bool CanParent(Control theControl)
{
if (theControl is MultiPanePage)
return !Control.Contains(theControl);
else
return false;
}
Also, several methods related to drag-and-drop will be overridden. Because our control can only host pages, and nothing else, a drag-and-drop operation must be processed by the control's pages. For example, dragging and dropping a CheckBox
control must result in its placement on one of the pages, and not the container. This is achieved by redirecting drag-and-drop operations to the page's designer, which we will cover a bit later. Let's look at the OnDragDrop
override:
protected override void OnDragDrop(DragEventArgs theDragEvents)
{
MultiPanePageDesigner aDsgn_Sel = GetSelectedPageDesigner();
if (aDsgn_Sel != null)
aDsgn_Sel.InternalOnDragDrop(theDragEvents);
}
The MultiPanePageDesigner
's internal method – InternalOnDragDrop
– will do the work for us. The same is true for the OnDragEnter
, OnDragLeave
, OnDragOver
, and OnGiveFeedback
methods.
In the GetSelectedPageDesigner
method, we will use a mechanism of setting Page visibility that is similar to the one we've defined in the SelectedPage
property of the control. However, in the MultiPaneControlDesigner
, this mechanism will only be applicable to design-time, allowing us to switch from one page to another, yet leaving the SelectedPage
property of the control unchanged.
To provide a means of synchronizing both properties, we will introduce two more events in MultiPaneControl
and slightly modify the body of its SelectedPage
set block:
public event EventHandler SelectedPageChanging;
public event EventHandler SelectedPageChanged;
public MultiPanePage SelectedPage
{
get { return mySelectedPage; }
set
{
if (mySelectedPage == value)
return;
if (SelectedPageChanging != null)
SelectedPageChanging(this, EventArgs.Empty);
if (mySelectedPage != null)
mySelectedPage.Visible = false;
mySelectedPage = value;
if (mySelectedPage != null)
mySelectedPage.Visible = true;
if (SelectedPageChanged != null)
SelectedPageChanged(this, EventArgs.Empty);
}
}
Just as with designer services, we can specify handlers for our new MultiPaneControl
events in the Initialize
method. Again, corresponding clean-up code needs to be added into the Dispose
method:
DesignedControl.SelectedPageChanged += new EventHandler(Handler_SelectedPageChanged);
DesignedControl.SelectedPageChanged -= new EventHandler(Handler_SelectedPageChanged);
Processing of the event is trivial:
private void Handler_SelectedPageChanged(object theSender, EventArgs theArgs)
{
mySelectedPage = DesignedControl.SelectedPage;
}
mySelectedPage
is a MultiPaneControl
's field holding the page visible at design time. Finally, the implementation of the GetSelectedPageDesigner
method goes like this:
private MultiPanePageDesigner GetSelectedPageDesigner()
{
MultiPanePage aSelPage = mySelectedPage;
if (aSelPage == null)
return null;
MultiPanePageDesigner aDesigner = null;
IDesignerHost aSrv = (IDesignerHost)GetService(typeof(IDesignerHost));
if (aSrv != null)
aDesigner = (MultiPanePageDesigner)aSrv.GetDesigner(aSelPage);
return aDesigner;
}
Working with Pages
This is perhaps the most interesting and rewarding part of the whole project, as we finally get to see our control and designer in action.
Our Handler_AddPage
needs to perform the following:
- Create a page.
- Add it to the collection of controls.
- Make the page visible for further editing.
- Reflect the changes into the form's source code.
private void Handler_AddPage(object theSender, EventArgs theArgs)
{
IDesignerHost aHost = (IDesignerHost)GetService(typeof(IDesignerHost));
if (aHost == null)
return;
MultiPaneControl aCtl = DesignedControl;
MultiPanePage aNewPage = (MultiPanePage)aHost.CreateComponent(typeof(MultiPanePage));
MemberDescriptor aMem_Controls = TypeDescriptor.GetProperties(aCtl)["Controls"];
RaiseComponentChanging(aMem_Controls);
aCtl.Controls.Add(aNewPage);
DesignerSelectedPage = aNewPage;
RaiseComponentChanged(aMem_Controls, null, null);
}
In order for the source code to reflect the changes, we use aHost.CreateComponent(typeof(MultiPanePage))
instead of new MultiPanePage()
. The two notification methods, RaiseComponentChanging
and RaiseComponentChanged
, are called for code serialization.
In theory, the above listing should work fine. But... what if something does go wrong, causing an exception somewhere in the middle of the execution? Consider this:
RaiseComponentChanging(aMem_Controls);
aCtl.Controls.Add(aNewPage);
throw new Exception("Test exception");
DesignerSelectedPage = aNewPage;
RaiseComponentChanged(aMem_Controls, null, null);
The first solution that comes to mind is wrapping the exception-prone code into a try
block. However, in our example, the code that executed prior to throwing an exception has already altered the state of our controls and reversing this in a catch
block would be extremely difficult.
To address this issue, we will initiate a transaction which will control the execution of our code. The transaction will only commit upon successful execution, and roll back otherwise. Microsoft documentation states that transactions are used for undo/redo support, but, frankly, I was unable to reproduce the lack of such support in a transaction-less environment.
I highly recommend adopting a uniform design strategy for methods that are to be executed inside transactions. That way, a single wrapper method could be used to initiate and commit all transactions, as well as to call a delegate to perform actual processing. Our Step3 example features a small class named DesignerTransactionUtility
which serves precisely that purpose:
public abstract class DesignerTransactionUtility
{
public static object DoInTransaction(
IDesignerHost theHost,
string theTransactionName,
TransactionAwareParammedMethod theMethod,
object theParam)
{
DesignerTransaction aTran = null;
object aRetVal = null;
try
{
aTran = theHost.CreateTransaction(theTransactionName);
aRetVal = theMethod(theHost, theParam);
}
catch (CheckoutException theEx)
{
if (theEx != CheckoutException.Canceled)
throw theEx;
}
catch
{
if (aTran != null)
{
aTran.Cancel();
aTran = null;
}
throw;
}
finally
{
if (aTran != null)
aTran.Commit();
}
return aRetVal;
}
}
The TransactionAwareParammedMethod
is a delegate that encapsulates an operation whose execution occurs inside a transaction:
public delegate object TransactionAwareParammedMethod(IDesignerHost theHost,
object theParam);
Then, we slightly modify the event handler:
private void Handler_AddPage(object theSender, EventArgs theArgs)
{
IDesignerHost aHost = (IDesignerHost)GetService(typeof(IDesignerHost));
if (aHost == null)
return;
DesignerTransactionUtility.DoInTransaction
(
aHost,
"MultiPaneControlAddPage",
new TransactionAwareParammedMethod(Transaction_AddPage),
null
);
}
private object Transaction_AddPage(IDesignerHost theHost, object theParam)
{
MultiPaneControl aCtl = DesignedControl;
MultiPanePage aNewPage = (MultiPanePage)theHost.CreateComponent(typeof(MultiPanePage));
MemberDescriptor aMem_Controls = TypeDescriptor.GetProperties(aCtl)["Controls"];
RaiseComponentChanging(aMem_Controls);
aCtl.Controls.Add(aNewPage);
DesignerSelectedPage = aNewPage;
RaiseComponentChanged(aMem_Controls, null, null);
return null;
}
We will apply the same strategy to the Toolbox item, modifying its code accordingly.
Page removal is also performed inside a transaction:
private void Handler_RemovePage(object theSender, EventArgs theEvent)
{
IDesignerHost aHost = (IDesignerHost)GetService(typeof(IDesignerHost));
if (aHost == null)
return;
DesignerTransactionUtility.DoInTransaction
(
aHost,
"MultiPaneControlRemovePage",
new TransactionAwareParammedMethod(Transaction_RemovePage),
null
);
}
In Transaction_RemovePage
, we destroy the page that is currently being designed. Before the page is destroyed, the ComponentRemoving
event is fired, and its handler, Handler_ComponentRemoving
, is called:
private void Handler_ComponentRemoving(object theSender, ComponentEventArgs theArgs)
{
IDesignerHost aHost = (IDesignerHost)GetService(typeof(IDesignerHost));
DesignerTransactionUtility.DoInTransaction
(
aHost,
"MultiPaneControlRemoveComponent",
new TransactionAwareParammedMethod(Transaction_UpdateSelectedPage)
);
}
private object Transaction_UpdateSelectedPage(IDesignerHost theHost, object theParam)
{
MultiPaneControl aCtl = DesignedControl;
MultiPanePage aPgTemp = mySelectedPage;
int aCurIndex = aCtl.Controls.IndexOf(mySelectedPage);
if (mySelectedPage == aCtl.SelectedPage)
{
MemberDescriptor aMember_SelectedPage =
TypeDescriptor.GetProperties(aCtl)["SelectedPage"];
RaiseComponentChanging(aMember_SelectedPage);
if (aCtl.Panes.Count > 1)
{
if (aCurIndex == aCtl.Panes.Count - 1)
aCtl.SelectedPage = aCtl.Panes[aCurIndex - 1];
else
aCtl.SelectedPage = aCtl.Panes[aCurIndex + 1];
}
else
aCtl.SelectedPage = null;
RaiseComponentChanged(aMember_SelectedPage, null, null);
}
else
{
if (aCtl.Panes.Count > 1)
{
if (aCurIndex == aCtl.Panes.Count - 1)
DesignerSelectedPage = aCtl.Panes[aCurIndex - 1];
else
DesignerSelectedPage = aCtl.Panes[aCurIndex + 1];
}
else
DesignerSelectedPage = null;
}
return null;
}
For page switching, we will create a handler that displays a choice dialog box. The implementation is trivial, and the dialog looks like this:
MultiPanePageDesigner
Our MultiPanePageDesigner
derives from ScrollableControlDesigner
. It handles drag-and-drop and drawing operations while redirecting mouse selection and designer verbs to MultiPaneControlDesigner
.
The class has three fields for redirecting mouse selection:
private int myOrigX = -1;
private int myOrigY = -1;
private bool myMouseMovement = false;
The OnMouseDragMove
method determines if mouse movement has taken place. OnMouseDragEnd
does the rest by either selecting the parent MultiPaneControl
or calling its base class method. The latter occurs if the user is, in fact, not intending to make a selection:
protected override void OnMouseDragBegin(int theX, int theY)
{
myOrigX = theX;
myOrigY = theY;
}
protected override void OnMouseDragMove(int theX, int theY)
{
if ( theX > myOrigX + 3 || theX < myOrigX - 3 ||
theY > myOrigY + 3 || theY < myOrigY - 3 )
{
myMouseMovement = true;
base.OnMouseDragBegin(myOrigX, myOrigY);
base.OnMouseDragMove(theX, theY);
}
}
protected override void OnMouseDragEnd(bool theCancel)
{
bool aProcess = !myMouseMovement && Control.Parent != null;
if (aProcess)
{
ISelectionService aSrv = (ISelectionService)GetService(typeof(ISelectionService));
if (aSrv != null)
aSrv.SetSelectedComponents(new Control[] { Control.Parent });
else
aProcess = false;
}
if (!aProcess)
base.OnMouseDragEnd(theCancel);
myMouseMovement = false;
}
The Verbs
property is also overridden. The DesignerVerbCollection
that it returns is populated from the base class designer and from the designer of the parent MultiPaneControl
:
public override DesignerVerbCollection Verbs
{
get
{
DesignerVerbCollection aRet = new DesignerVerbCollection();
foreach ( DesignerVerb aIt in base.Verbs)
aRet.Add(aIt);
MultiPaneControlDesigner aDs = GetParentControlDesigner();
if (aDs != null)
foreach (DesignerVerb aIt in aDs.Verbs)
aRet.Add(aIt);
return aRet;
}
}
Drag-and-drop operations that are carried over from MultiPaneControlDesigner
are also processed in the MultiPanePaneDesigner
class. Note that we don't have to do anything special as ScrollableControlDesigner
does all the processing for us automatically:
internal void InternalOnDragDrop(DragEventArgs theArgs)
{ OnDragDrop(theArgs); }
internal void InternalOnDragEnter(DragEventArgs theArgs)
{ OnDragEnter(theArgs); }
internal void InternalOnDragLeave(EventArgs theArgs)
{ OnDragLeave(theArgs); }
internal void InternalOnGiveFeedback(GiveFeedbackEventArgs theArgs)
{ OnGiveFeedback(theArgs); }
internal void InternalOnDragOver(DragEventArgs theArgs)
{ OnDragOver(theArgs); }
To render page borders on the form, we will override the OnPaintAdornments
method. Our implementation will draw a dashed rectangle, making our pages look similar to those of a Tab Control:
protected override void OnPaintAdornments(PaintEventArgs theArgs)
{
DrawBorder(theArgs.Graphics);
base.OnPaintAdornments(theArgs);
}
protected void DrawBorder(Graphics theG)
{
MultiPanePage aCtl = DesignedControl;
if (aCtl == null)
return;
else if (!aCtl.Visible)
return;
Rectangle aRct = aCtl.ClientRectangle;
aRct.Width--;
aRct.Height--;
theG.DrawRectangle(BorderPen, aRct);
}
- and we're done! The final version is in the Step3 solution.
A Few Final Remarks
- All sample code has been tested under Visual Studio .NET 2003, Visual Studio 2005, and Visual Studio 2008 Beta 2.
- Project files provided are of version 2003. Use the Project Import Wizard to open the project in higher versions of Visual Studio.
- Run Rebuild Solution before opening sample forms in Design mode.
The supplied source code can be freely distributed and included, in whole or in part, with any third-party software product on the condition that my authorship is properly acknowledged and explicitly stated in the Copyright section.
Comments, suggestions, and bug reports are greatly appreciated. Any ideas for improvement are also welcome.
History
- 2007-11-25. Initial release
- 2007-11-30. A few typo corrections have been made
- 2007-12-08. Due to CodeProject's bugs with page serving, all article files (graphics and ZIP archives) were moved to a mirror site
- 2009-03-31. Added support for background transparency