Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Multipanel Control in C#

0.00/5 (No votes)
17 Jun 2009 5  
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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here