My approach, after checking several options, was to inherit the TabControl component in such a way the pages of the control can be used as paged panels, and adding functionality so that the tabs do not show at run time. Then, by creating a property called Pages which depends on TabPages, I can refer to each page in a semantically correct way, giving the advantage of being able to manage every page as part of the Pages collection, and also hierarchically through the document explorer.
The code also hides design-time properties that pertain to a regular TabControl, but that would be irrelevant in a paged panel. Below is the code if anyone is interested.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.ComponentModel;
using System.Drawing;
namespace MyCustomControls
{
public class PagedPanel : TabControl
{
//------------------------------------------------------------------------------------------------
public PagedPanel()
{
base.Multiline = true;
base.Appearance = TabAppearance.Buttons;
base.ItemSize = new Size(0, 1);
base.SizeMode = TabSizeMode.Fixed;
base.TabStop = false;
}
//------------------------------------------------------------------------------------------------
protected override void WndProc(ref Message m)
{
// Hide tabs by trapping the TCM_ADJUSTRECT message
if (m.Msg == 0x1328 && !DesignMode) m.Result = (IntPtr)1;
else base.WndProc(ref m);
}
//------------------------------------------------------------------------------------------------
protected override void OnKeyDown(KeyEventArgs ke)
{
// Block Ctrl+Tab and Ctrl+Shift+Tab hotkeys
if (ke.Control && ke.KeyCode == Keys.Tab)
return;
base.OnKeyDown(ke);
}
//------------------------------------------------------------------------------------------------
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[DefaultValue(true)]
public new bool Multiline
{
get { return base.Multiline; }
set { base.Multiline = value; Invalidate(); }
}
//------------------------------------------------------------------------------------------------
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)
, DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[DefaultValue(TabAppearance.Buttons)]
public new TabAppearance Appearance
{
get { return base.Appearance; }
set { base.Appearance = value; Invalidate(); }
}
//------------------------------------------------------------------------------------------------
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)
, DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[DefaultValue(typeof(Size), "0, 1")]
public new Size ItemSize
{
get { return base.ItemSize; }
set { base.ItemSize = value; Invalidate(); }
}
//------------------------------------------------------------------------------------------------
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)
, DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[DefaultValue(TabSizeMode.Fixed)]
public new TabSizeMode SizeMode
{
get { return base.SizeMode; }
set { base.SizeMode = value; Invalidate(); }
}
//------------------------------------------------------------------------------------------------
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)
, DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new TabPageCollection TabPages
{
get { return base.TabPages; }
}
//------------------------------------------------------------------------------------------------
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[DefaultValue(false)]
public new bool TabStop
{
get { return base.TabStop; }
set { base.TabStop = value; Invalidate(); }
}
//------------------------------------------------------------------------------------------------
public TabPageCollection Pages
{
get { return base.TabPages; }
}
//------------------------------------------------------------------------------------------------
}
}
The treeview would handle calling each tab either by key or index, a relatively trivial task. I do this by naming the nodes in my tree with a prefix such as "tvn", and then naming the pages in the PagedPanel the same but with prefix "pg". So on the AfterSelect event of the treeview, all I need is the name of the current node and I know what page to show.