Click here to Skip to main content
Click here to Skip to main content

A Multipanel Control in C#

, 17 Jun 2009 Public Domain
Rate this:
Please Sign up or sign in to vote.
This control acts like a tab control but without the tabs...

Introduction

For a long time, I needed a control that can act just like a tab control but without displaying any tabs. This control can be very useful because you can use the powerful forms designer to add many different pages in the same form. Otherwise you are forced to add a different user control for every page, which can be a real pain in the ass.

Unfortunately I didn't find one. In the past I even used a TabControl and pasted an ugly panel above the tabs so they won't be seen. Ugly Ugly Ugly .. 

In my final project, I needed one again and I've decided to solve this problem once and for all.

Background 

Trying to be as lazy as I can, I've searched the Internet for something that I can copy paste with a minimal number of changes to my new control. 

After a few minutes, I found what I was looking for - an article on this very site http://www.codeproject.com/KB/miscctrl/yatabcontrol.aspx written by curtis schlak. This great article provided the basis for my control. 

Code Description

The code is divided into several classes:

  • The MultiPanel control - a very simple class that derives from the Panel class and holds the collection of pages and the currently selected page instance.
  • The MultiPanelPagesCollection class - derives from the ControlCollection class in order to force all contained controls to be of type MultiPanelPage control. 
  • The MultiPanelPage control - provides the implementation for all multi panel pages. Derives from ContainerControl in order to act as a container for all controls that we drag into it.
  • Two designer classes: MultiPanelDesigner needed for designing the MultiPanel control, and MultiPanelPageDesigner needed for designing the multipanelpage control (and also drawing the page's Text property for readability). 

As you can see - the MultiPanel class is very simple. Basically it is a panel that replaces the selected page control based on the value stored in _selectedPage variable:

 [ToolboxBitmap(typeof(MultiPanel), "multipanel")]
    [Designer(typeof(Liron.Windows.Forms.Design.MultiPanelDesigner))]
    public class MultiPanel : Panel
    {
        public MultiPanelPage SelectedPage
        {
            get { return _selectedPage; }
            set
            {
                _selectedPage = value;
                if (_selectedPage != null)
                {
                    foreach (Control child in Controls)
                    {
                        if (object.ReferenceEquals(child, _selectedPage))
                            child.Visible = true;
                        else
                            child.Visible = false;
                    } // foreach
                }
            }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            Graphics g = e.Graphics;

            using (SolidBrush br = new SolidBrush(BackColor))
                g.FillRectangle(br, ClientRectangle);
        }

        protected override ControlCollection CreateControlsInstance()
        {
            return new MultiPanelPagesCollection(this);
        }

        private MultiPanelPage _selectedPage;
    } 

Note that it declares the type of its designer in the class level Designer attribute.

Also note that we've replaced the class used for holding controls by overriding the CreateControlsInstance() method. 

The MultiPanelPage class is very simple as well. Basically - I've overridden the controls collection class with my own class in order to prevent MultiPanelPage instance from being inserted into the multi panel pages and I've declared the design class for the page class: 

[Designer(typeof(Liron.Windows.Forms.Design.MultiPanelPageDesigner))]
    public class MultiPanelPage : ContainerControl
    {
        public MultiPanelPage()
        {
            base.Dock = DockStyle.Fill;
        }

        /// <summary>
        /// Overridden from <see cref="Panel"/>.
        /// </summary>
        /// <remarks>
        /// Since the <see cref="MultiPanelPage"/> exists only
        /// in the context of a <see cref="MultiPanelControl"/>,
        /// it makes sense to always have it fill the
        /// <see cref="MultiPanelControl"/>. Hence, this property
        /// will always return <see cref="DockStyle.Fill"/>
        /// regardless of how it is set.
        /// </remarks>
        public override DockStyle Dock
        {
            get
            {
                return base.Dock;
            }
            set
            {
                base.Dock = DockStyle.Fill;
            }
        }

        /// <summary>
        /// Only here so that it shows up in the property panel.
        /// </summary>
        public override string Text
        {
            get
            {
                return base.Text;
            }
            set
            {
                base.Text = value;
            }
        }

        /// <summary>
        /// Overridden from <see cref="Control"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="MultiPanelPage.ControlCollection"/>.
        /// </returns>
        protected override 
	System.Windows.Forms.Control.ControlCollection CreateControlsInstance()
        {
            return new MultiPanelPage.ControlCollection(this);
        }

        #region Classes
        public new class ControlCollection : Control.ControlCollection
        {
            /// <summary>
            /// </summary>
            public ControlCollection(Control owner)
                : base(owner)
            {
                if (owner == null)
                    throw new ArgumentNullException("owner", 
			"Tried to create a MultiPanelPage.ControlCollection 
			with a null owner.");
                MultiPanelPage c = owner as MultiPanelPage;
                if (c == null)
                    throw new ArgumentException("Tried to create a 
			MultiPanelPage.ControlCollection with a 
			non-MultiPanelPage owner.", "owner");
            }

            /// <summary>
            /// </summary>
            public override void Add(Control value)
            {
                if (value == null)
                    throw new ArgumentNullException("value", 
			"Tried to add a null value to the 
			MultiPanelPage.ControlCollection.");
                MultiPanelPage p = value as MultiPanelPage;
                if (p != null)
                    throw new ArgumentException("Tried to add a 
			MultiPanelPage control to the 
			MultiPanelPage.ControlCollection.", "value");
                base.Add(value);
            }
        }
        #endregion
    } 

The MultiPanelDesigner class is responsible for allowing the user to add and remove panel pages. This is done by defining support for two verbs:

/// <summary>
/// Overridden. Inherited from <see cref="ControlDesigner"/>.
/// </summary>
public override DesignerVerbCollection Verbs
{
    get
    {
        if (_verbs == null)
        {
            _verbs = new DesignerVerbCollection();
            _verbs.Add(new DesignerVerb("Add Page", new EventHandler(AddPage)));
            _verbs.Add(new DesignerVerb("Remove Page", new EventHandler(RemovePage)));
        }
        return _verbs;
    }
} 

And defining them as follows: 

 private void AddPage(object sender, EventArgs ea)
        {
            IDesignerHost dh = (IDesignerHost)GetService(typeof(IDesignerHost));
            if (dh != null)
            {
                DesignerTransaction dt = dh.CreateTransaction("Added new page");

                MultiPanelPage before = _mpanel.SelectedPage;

                string name = GetNewPageName();
                MultiPanelPage ytp = dh.CreateComponent(typeof(MultiPanelPage), 
						name) as MultiPanelPage;
                ytp.Text = name;
                _mpanel.Controls.Add(ytp);
                _mpanel.SelectedPage = ytp;

                RaiseComponentChanging(TypeDescriptor.GetProperties(Control)
							["SelectedPage"]);
                RaiseComponentChanged(TypeDescriptor.GetProperties(Control)
						["SelectedPage"], before, ytp);

                dt.Commit();
            }
        } 
private void RemovePage(object sender, EventArgs ea)
    {
        IDesignerHost dh = (IDesignerHost)GetService(typeof(IDesignerHost));
        if (dh != null)
        {
            DesignerTransaction dt = dh.CreateTransaction("Removed page");

            MultiPanelPage page = _mpanel.SelectedPage;
            if (page != null)
            {
                MultiPanelPage ytp = _mpanel.SelectedPage;
                _mpanel.Controls.Remove(ytp);
                dh.DestroyComponent(ytp);

                if (_mpanel.Controls.Count > 0)
                    _mpanel.SelectedPage = (MultiPanelPage)_mpanel.Controls[0];
                else
                    _mpanel.SelectedPage = null;

                RaiseComponentChanging(TypeDescriptor.GetProperties
			(Control)["SelectedPage"]);
                RaiseComponentChanged(TypeDescriptor.GetProperties
			(Control)["SelectedPage"], ytp, _mpanel.SelectedPage);
            }

            dt.Commit();
        }
    }

    /// <summary>
    /// Gets a new page name for the a page.
    /// </summary>
    /// <returns></returns>
    private string GetNewPageName()
    {
        int i = 1;
        Hashtable h = new Hashtable(_mpanel.Controls.Count);
        foreach (Control c in _mpanel.Controls)
        {
            h[c.Name] = null;
        }
        while (h.ContainsKey("Page_" + i))
        {
            i++;
        }
        return "Page_" + i;
    } 

Note that I'm using the GetNewPageName() method in order to create a new name for the page control, and that the designer interacts with the underlying multipanel class in order to add the new page and select it.

Finally - there is the MultiPanelPageDesigner class that is responsible for managing design time interaction with the MultiPanelPage control. This class is responsible for drawing the Text property of the underlying page control (OnPaintAdornments method) and handle changes in the Text property of the page control (done by shadowing the Text property of the underlying page control).

public class MultiPanelPageDesigner : ScrollableControlDesigner
    {
        public MultiPanelPageDesigner()
        {
        }

        /// <summary>
        /// Shadows the <see cref="MultiPanelPage.Text"/> property.
        /// </summary>
        public string Text
        {
            get
            {
                return _page.Text;
            }
            set
            {
                string ot = _page.Text;
                _page.Text = value;
                IComponentChangeService iccs = 
		GetService(typeof(IComponentChangeService)) 
			as IComponentChangeService;
                if (iccs != null)
                {
                    MultiPanel ytc = _page.Parent as MultiPanel;
                    if (ytc != null)
                        ytc.Refresh();
                }
            }
        }

        /// <summary>
        /// Overridden. Inherited from
        /// <see cref="ControlDesigner.OnPaintAdornments(PaintEventArgs)"/>.
        /// </summary>
        /// <param name="pea">
        /// Some <see cref="PaintEventArgs"/>.
        /// </param>
        protected override void OnPaintAdornments(PaintEventArgs pea)
        {
            base.OnPaintAdornments(pea);

            // My thanks to bschurter (Bruce), CodeProject member #1255339 for this!
            using (Pen p = new Pen(SystemColors.ControlDark, 1))
            {
                p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
                pea.Graphics.DrawRectangle(p, 0, 0, _page.Width - 1, _page.Height - 1);
            }

            using (Brush b = new SolidBrush(Color.FromArgb(100, Color.Black)))
            {
                float fh = _font.GetHeight(pea.Graphics);
                RectangleF tleft = new RectangleF(0, 0, _page.Width / 2, fh);
                RectangleF bleft = new RectangleF(0, _page.Height - fh, _
						page.Width / 2, fh);
                RectangleF tright = new RectangleF(_page.Width / 2, 0, 
						_page.Width / 2, fh);
                RectangleF bright = new RectangleF(_page.Width / 2, 
				_page.Height - fh, _page.Width / 2, fh);
                pea.Graphics.DrawString(_page.Text, _font, b, tleft);
                pea.Graphics.DrawString(_page.Text, _font, b, bleft);
                pea.Graphics.DrawString(_page.Text, _font, b, tright, _rightfmt);
                pea.Graphics.DrawString(_page.Text, _font, b, bright, _rightfmt);
            }
        }

        /// <summary>
        /// Overridden. Inherited from 
        /// <see cref="ControlDesigner.Initialize( IComponent )"/>.
        /// </summary>
        /// <param name="component">
        /// The <see cref="IComponent"/> hosted by the designer.
        /// </param>
        public override void Initialize(IComponent component)
        {
            _page = component as MultiPanelPage;
            if (_page == null)
                DisplayError(new Exception("You attempted to use a 
		MultiPanelPageDesigner with a class that does not 
		inherit from MultiPanelPage."));
            base.Initialize(component);
        }

        /// <summary>
        /// Overridden. Inherited from 
        /// <see cref="ControlDesigner.PreFilterProperties(IDictionary)"/>.
        /// </summary>
        /// <param name="properties"></param>
        protected override void PreFilterProperties(IDictionary properties)
        {
            base.PreFilterProperties(properties);
            properties["Text"] = TypeDescriptor.CreateProperty
		(typeof(MultiPanelPageDesigner), (PropertyDescriptor)properties
				["Text"], new Attribute[0]);
        }        

        /// <summary>
        /// </summary>
        private MultiPanelPage _page;
        private Font _font = new Font("Courier New", 8F, FontStyle.Bold);
        private StringFormat _rightfmt = new StringFormat
	(StringFormatFlags.NoWrap | StringFormatFlags.DirectionRightToLeft);
    } 

Using the Code

If you are like me - this is the section that you want to read first (actually - this is the only section you'll want to read...).

Using this control is very simple. You simply drag it into the form, use the AddPage verb to add new pages or the RemovePage verb to remove an existing page. 

Once a page is added - you'll notice that the page's Text property appears on all the four sides of the page. This text appears only in design time and is very useful for knowing which page is currently selected in the multipanel control. 

The way I like to work is to open the document-outline view (like is shown in the screenshot above) and jump between the various pages using the mouse. You can open the document-outline view from the View/Other Windows/Document Outline menu.

Now you can drag  various controls into the various pages and switch them in design time using the document outline view. 

OK - this solves the design time problem. When you want to use the control in runtime - you'll use the multipanel control's SelectedPage in order to change the currently displayed page. The test application contains a very short code to demonstrate this. 

That's it! I hope it will save you some time and make your life a bit easier. 

History

  • v0.1 17-June-2009 - Initial version 

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication

Share

About the Author

liron.levi
Software Developer Arineta Cardio Imaging
Israel Israel
My name is Liron Levi and I'm developing software for fun & profit for 15 years already. I'm now working for Arineta Cardio Imaging as a software developer (the company develops a CT machine).
 
I can be contacted directly at liron.levi@outlook.com or via linkedin at http://www.linkedin.com/pub/liron-levy/1/578/ab5

Comments and Discussions

 
QuestionCannot select timer properties PinmemberPanupong Waesuntie3-Oct-13 16:01 
AnswerRe: Cannot select timer properties PinmemberPanupong Waesuntie3-Oct-13 16:15 
BugVS Crashes when moving PagePanel into another PagePanel [modified] PinmemberBCantor18-Jun-13 7:52 
GeneralRe: VS Crashes when moving PagePanel into another PagePanel Pinmemberliron.levi18-Jun-13 19:59 
GeneralRe: VS Crashes when moving PagePanel into another PagePanel PinmemberBCantor19-Jun-13 8:16 
GeneralRe: VS Crashes when moving PagePanel into another PagePanel Pinmemberbkraul3-Feb-14 4:39 
GeneralRe: VS Crashes when moving PagePanel into another PagePanel PinmemberMacSpudster5-Feb-14 8:02 
QuestionThe Perfect Control I didn't use PinmemberBCantor18-Jun-13 7:03 
AnswerRe: The Perfect Control I didn't use Pinmemberliron.levi18-Jun-13 19:56 
GeneralMultipanel Control PinmemberRickiB29-Apr-13 7:11 
GeneralRe: Multipanel Control Pinmemberliron.levi29-Apr-13 19:22 
QuestionVery nice control! PinmemberMatt-HH21-Sep-12 11:50 
QuestionGoo job PinmemberMike Hankey29-Aug-11 18:04 
Generalgood article PinmemberCIDev14-Jun-11 4:23 
GeneralRe: good article Pinmemberliron.levi14-Jun-11 7:18 
GeneralRe: good article PinmemberCIDev16-Jun-11 14:59 
GeneralThis is best comment PinmemberIs_Neo15-Mar-11 4:30 
GeneralRe: This is best comment Pinmemberliron.levi15-Mar-11 9:20 
GeneralMy vote of 5 PinmemberFishbox31-Jan-11 9:57 
GeneralMultiPanel Control PinmemberDenville13-Dec-10 0:04 
GeneralRe: MultiPanel Control Pinmemberliron.levi14-Dec-10 20:16 
GeneralRe: MultiPanel Control PinmemberDenville14-Dec-10 22:45 
GeneralThank you Pinmembermeir.shaull16-Nov-10 5:15 
GeneralRe: Thank you Pinmemberliron.levi16-Nov-10 21:49 
GeneralAppreciated! learned a lot PinmemberYankee Imperialist Dog!7-Nov-10 5:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.141022.2 | Last Updated 17 Jun 2009
Article Copyright 2009 by liron.levi
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid