Hi, I'm Ray

I'm a software developer, full-time nerd and occasional human (while heavily caffeinated).

C#: Building a TNoteBook component for Windows Forms as an user control (PanelBook)

Last week I was developing a Windows Forms (WinForms) application that required a particular kind of user interface. The application was a step-by-step wizard that had navigation buttons: “back” and “next”. Think multiple steps of a setup installer. Each page occupied the same position and contains other controls.

I wanted a more elegant solution other than hiding/showing and moving panels around the form.

I considered the common controls available:

  • FlowLayoutPanel/TableLayoutPanel
    Each page would be a panel in the layout being shown or hidden. The control would handle the positioning but the designing through the form designer would be unpleasant.

  • SplitContainer
    Each page would also be a panel and you’d still be changing the states of the various panels with extra code. The form designer experience should be improved as you can hide he panels you aren’t working on.

  • TabControl
    The solution really calls for a TabControl but without the tabs. Unfortunately the tabs aren’t easily removable.

This triggered a memory from around 11 years ago. I started my quest down the programming skill-tree with Pascal (a tradition, at the time); this led to Visual Pascal, Borland Delphi and C++ Builder. Within the Visual Component Library (VCL) and Lazarus Component Library (LCL), there is a control (visual component) called TNoteBook. This visual component is a control that contains a collection of pages. Only one page is visible at a time and pages can be show by its index in the collection. Pages can contain other controls. This is, in effect, a TabControl without tabs.

All those years ago I had started building a user control to be my TNoteBook for .NET. Unfortunately, I had two problems:

  1. I really didn’t have a clue with what I was doing
  2. I was a garbage-monster who gave up when met with mild resistance

Fast-forward to last week, the more-mature-but-still-garbage Ray built it in under an hour. I present to you all the “PanelBook”, a work-in-progress. This is not a lesson in how procrastinating for 11 years makes things okay in the end.

 

Let’s get started

 

1. PanelCollection

The heart of the PanelBook is the container collection. I’ve chosen to use a Panel but you can use a custom user control.


namespace RyzStudio.Windows.Forms
{
  using System.Collections;
  using System.ComponentModel;
  using System.Windows.Forms;

  [ToolboxItem(true)]
  public class PanelCollection : CollectionBase
  {
    protected PanelBook panelBook = null;

    public PanelCollection()
    {

    }

    public PanelCollection(PanelBook parent) : base()
    {
      panelBook = parent;
    }

    public PanelBook Parent => panelBook;

    public Panel this[int index] { get => (Panel)List[index]; set => List[index] = value; }

    public int Add(Panel value) => List.Add(value);

    public void AddRange(Panel[] pages)
    {
      foreach (Panel page in pages)
      {
        this.Add(page);
      }
    }

    public int IndexOf(Panel value) => (List.IndexOf(value));

    public void Insert(int index, Panel value) => List.Insert(index, value);

    public void Remove(Panel value) => List.Remove(value);

    public bool Contains(Panel value) => List.Contains(value);

    protected override void OnInsertComplete(int index, object value)
    {
      base.OnInsertComplete(index, value);

      if (panelBook != null)
      {
        panelBook.PageIndex = index;
      }
    }

    protected override void OnRemoveComplete(int index, object value)
    {
      base.OnRemoveComplete(index, value);

      if (panelBook != null)
      {
        if (panelBook.PageIndex == index)
        {
          if (index < InnerList.Count)
          {
            panelBook.PageIndex = index;
          }
          else
          {
            panelBook.PageIndex = InnerList.Count - 1;
          }
        }
      }
    }

  }
}

 

2. The Rest

The user control is actually pretty straight-forward, even simple.


namespace RyzStudio.Windows.Forms
{
  using System;
  using System.ComponentModel;
  using System.Windows.Forms;

  [ToolboxItem(true)]
  public partial class PanelBook : UserControl
  {
    protected PanelCollection panelCollection = null;

    public PanelBook()
    {
      InitializeComponent();

      panelCollection = new PanelCollection(this);

    }

    public Panel ActivePanel { get; set; } = null;

    [Category("Collection")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public PanelCollection Pages
    {
      get { return panelCollection; }
    }

    [Category("Collection")]
    public int SelectedIndex
    {
      get
      {
        if (panelCollection.Count <= 0)
        {
          return -1;
        }

        return panelCollection.IndexOf(this.ActivePanel);
      }
      set
      {
        if (panelCollection.Count <= 0)
        {
          return;
        }

        if (value < 0)
        {
          return;
        }

        if (value > (panelCollection.Count - 1))
        {
          return;
        }

        if (value == this.SelectedIndex)
        {
          return;
        }

        ActivatePanel(value);
      }
    }

    protected internal int PageIndex
    {
      get
      {
        return panelCollection.IndexOf(this.ActivePanel);
      }
      set
      {
        if (panelCollection.Count <= 0)
        {
          ActivatePanel(-1);
          return;
        }

        if ((value < -1) || (value >= panelCollection.Count))
        {
          throw new ArgumentOutOfRangeException("PageIndex", value, "The page index must be between 0 and " + Convert.ToString(panelCollection.Count - 1));
        }

        ActivatePanel(value);
      }
    }

    protected internal void ActivatePanel(int index)
    {
      if ((panelCollection.Count == 0) && (index >= panelCollection.Count) && (index <= 0))
      {
        return;
      }

      Panel p = (Panel)panelCollection[index];

      ActivatePage(p);
    }

    protected internal void ActivatePage(Panel page)
    {
      if (this.ActivePanel != null)
      {
        this.ActivePanel.Visible = false;
      }

      this.ActivePanel = page;
      if (this.ActivePanel != null)
      {
        this.ActivePanel.Parent = this;
        if (!this.Contains(this.ActivePanel))
        {
          this.Container.Add(this.ActivePanel);
        }

        this.ActivePanel.Dock = DockStyle.Fill;
        this.ActivePanel.Visible = true;
        this.ActivePanel.BringToFront();
      }

      if (this.ActivePanel != null)
      {
        this.ActivePanel.Invalidate();
      }
      else
      {
        this.Invalidate();
      }
    }

#if DEBUG

    protected override void OnResize(EventArgs e)
    {
      base.OnResize(e);

      if (this.DesignMode)
      {
        this.Invalidate();
      }
    }

#endif

    protected override void DestroyHandle()
    {
      base.DestroyHandle();

      foreach (Panel p in panelCollection)
      {
        p.Dispose();
      }
    }

  }
}

 

3. Remarks

There are two properties under “Collection”: Pages and SelectedIndex. Add panels to the Pages collection and change the page shown by changing the selected index to the collection item index. Use the form designer to add and edit controls as normal.

 

That is it.

I hope someone finds this useful.

PanelBook

The Author

Ray
  • Hi, I'm Ray. I'm a software developer, full-time nerd and occasional human (while heavily caffeinated).

Copyright © 2014-2019 Ray Lam. All Rights Reserved.