MDI interface alternative

As some developers might remember there was a time when Microsoft created/used the MDI type of user interface – MDI stands for Multiple Document Interface in case you forgot. It was widely in use during the Windows 3 and 95 areas and applications like Word, Excel and even Visual Studio used it. And then Microsoft move on… apparently abandoning the use for MDI in their own applications.

Fortunately (sort of) they still support developers creating MDI interface applications in Visual Studio 2010, but for WinForms only. WPF has no native support for it. The real problem is that since Windows 7/Vista came around they don’t actually support the MDI type of interface. Easiest way to see this is that MDI child windows are not rendered with the current Windows theme but instead shown with the ugly ‘default’ Windows theme. This is really a shame. There are still many applications or scenarios where this type of user interface is useful – I’ll admit there are many cases where alternative UI’s are better – like the tabs in web browsers like Firefox and lately IE. However, there is no ‘easy’ way to create applications using a tabbed interface in Visual Studio with all the supported things like Firefox or IE has. For one – dragging, moving and dropping tabs are not supported by default (as in WinForms – not sure about WPF). You cannot ’embed’ child Forms inside tabs (well, actually it might theoretically work as both ‘Forms’ and ‘Controls’ are internally derived from the same classes but it is going to cost some hacking…). Instead, you have to develop your entire Form as a UserControl, which is not a train smash but still…

So I’ve been digging through the Internet (electron by electron it feels like) and saw many others complain and suggesting alternatives. All the complaints aside, there are ways to do things differently but it is going to cost some work! (rolling up sleeves).

I started playing with some code to see what I can do with some trial and error. The basic approach I’m following is to use a plain old WinForm with a tab control on it. Creating tabs dynamically is easy enough in .Net. What is not so easy is the part where you must keep track of the child ‘windows’ and what is where at what time. Basically, you have to rewrite your own mini ‘Window manager’. I’ve got something working that kind of works but it doesn’t look pretty yet. It has the potential to develop into a full new MDI like framework. Keep in mind this is still work in progress and it is not complete.

IChildWindow

In order for the main or parent form to ‘communicate’ with the children and vice versa you need some standard interface. I created IChildWindow for this.

public interface IChildWindow
{

/// <summary>
/// Unique Id if used
/// </summary>
int Id { get; set; }
/// <summary>
/// Type description
/// </summary>
string Type { get; }
/// <summary>
/// Is the tab/Window pinned
/// </summary>
bool Pinned { get; set; }
/// <summary>
/// Start clean up process before closing tab/window
/// </summary>
void CloseAndCleanup();
string GetWindowTitle();
event StartCleanupDelegate StartCloseAndCleanup;
event PinnedStatusChangedDelegate PinnedStatusChanged;

}

This interface is really simple and small. The ‘Id’ and ‘Type’ properties are used to uniquely identify a ‘child window’. Actually, the ‘Id’ is only used for cases where the child can have multiple instances – say you have an editing window for persons then the ‘Id’ would be used for the person id field. In other cases you might want a window that is unique by itself in the application or reused for everything.

Pinned is used to ‘pin’ the window – if you choose ‘Close All tabs’ on the parent window it will not close the pinned tabs.

The CloseAndCleanup method is simply a way to to ensure any resources created/used on the child window gets cleaned up. GetWindowTitle is called to set the display name or text of the tab/window.

UserControlIdentifier

In order to ‘map’ type of ‘windows’ used to a name I created this simple class.

public class UserControlIdentifier
{

public string TypeName { get; set; }
public Type UserControl { get; set; }

}

It identifies the type of ‘window’ or in this case the UserControl.

WindowManager

This is a base class that manages the list of opened ‘windows’. It has methods for finding, creating, opening, and closing ‘windows’. It is generic and does not have a direct link to the type of ‘control’ used to host the new ‘window’ system. Instead, it only reference the IChildWindow interface (and UserControlIdentifier for identifying and creating windows).

The following is just a partial view of the class.

public abstract class WindowManager
{

private List<IChildWindow> windowList = new List<IChildWindow>();
protected abstract void CreateNewWindow(IChildWindow cw);
protected abstract void CloseWindow(IChildWindow cw);
protected abstract void SetWindowFocus(IChildWindow cw);
protected abstract void OnPinnedStatusChanged(IChildWindow cw);

#region Open window

public void OpenChildWindow(UserControlIdentifier uci, int id, bool reUseSimilarType)
{

IChildWindow c = FindChildWindowByTypeAndId(uci, id);
if (c != null)
{

SetWindowFocus(c);

}
else
{

if (reUseSimilarType)
{

c = FindFirstChildWindowOfType(uci);
if (c != null)
{

c.Id = id;
SetWindowFocus(c);
return;

}

}
//The create new window
IChildWindow newChild = CreateChildWindow(uci, id);

}

}
private IChildWindow CreateChildWindow(UserControlIdentifier uci, int id)
{

IChildWindow newChild = (IChildWindow)Activator.CreateInstance(uci.UserControl);
newChild.Id = id;
CreateNewWindow(newChild);
SetWindowFocus(newChild);
newChild.PinnedStatusChanged += new PinnedStatusChangedDelegate(OnPinnedStatusChanged);
newChild.Pinned = false; //and display the unpinned icon
windowList.Add(newChild);
return newChild;

}

#endregion

#region Close window

public void CloseChildWindow(IChildWindow cw)
{

cw.CloseAndCleanup();
windowList.Remove(cw);
CloseWindow(cw);

}

public void CloseAllChildWindows(bool forced)
{

for (int i = windowList.Count – 1; i >= 0; i–)
{

IChildWindow cw = windowList[i];
if (forced || (!cw.Pinned))
{

CloseChildWindow(cw);

}

}

}

#endregion

#region Find window
public IChildWindow FindFirstChildWindowOfType(UserControlIdentifier uci)
{

foreach (IChildWindow c in windowList)
{

if (c.Type == uci.TypeName)
{

return c;

}

}
return null;

}
public IChildWindow FindChildWindowByTypeAndId(UserControlIdentifier uci, int id)
{

foreach (IChildWindow c in windowList)
{

if (c.Type == uci.TypeName && c.Id == id)
{

return c;

}

}
return null;

}
#endregion

#region Pinned status
public void SetChildWindowPinnedStatus(IChildWindow c)
{

c.Pinned = !c.Pinned;

}
#endregion

}

WindowManagerForTabControl

This is the custom implementation of the WindowManager base class. It is created to handle the System.Windows.Forms.TabControl as ‘host’ to the ‘windows’ specifically.

/// <summary>
/// WindowManager implementation for System.Windows.Forms.TabControl
/// Note: the tab control must use an image list control with 3 images
///   image 0: fixed tab
///   image 1: unpinned tab
///   image 2: pinned tab
/// </summary>
public class WindowManagerForTabControl : WindowManager
{

private TabControl hostTabControl;
public WindowManagerForTabControl(TabControl hostTabControl)
{

this.hostTabControl = hostTabControl;

}

protected override void CreateNewWindow(IChildWindow cw)
{

TabPage tp = new TabPage(cw.GetWindowTitle());
((Control)cw).Dock = DockStyle.Fill;
tp.Controls.Add(((Control)cw));
tp.Tag = cw; //set inverse reference
hostTabControl.TabPages.Add(tp);
hostTabControl.SelectTab(tp); //and switch to it

}

protected override void CloseWindow(IChildWindow cw)
{

TabPage tp = null;
foreach (TabPage t in hostTabControl.TabPages)
{

if (t.Tag is IChildWindow && (IChildWindow)t.Tag == cw)
{

tp = t;

}

}
if (tp != null)

hostTabControl.TabPages.Remove(tp);

}

protected override void SetWindowFocus(IChildWindow cw)
{

TabPage tp = null;
foreach (TabPage t in hostTabControl.TabPages)
{

if (t.Tag is IChildWindow && (IChildWindow)t.Tag == cw)
{

tp = t;

}

}
if (tp != null)
{

hostTabControl.SelectTab(tp);
tp.Text = cw.GetWindowTitle();

}

}

protected override void OnPinnedStatusChanged(IChildWindow cw)
{

TabPage tp = null;
foreach (TabPage t in hostTabControl.TabPages)
{

if (t.Tag is IChildWindow && (IChildWindow)t.Tag == cw)
{

tp = t;

}

}
if (tp != null)
{

if (cw.Pinned)

tp.ImageIndex = 1;

else

tp.ImageIndex = 2;

}

}

public IChildWindow GetChildWindowFromTabPage(TabPage tabPage)
{

if (tabPage.Tag is IChildWindow)

return (IChildWindow)tabPage.Tag;

else

return null;

}

}

If you want to use another control or something like WPF’s tab control you would create a similar class for those types.

GlobalChildWindowController

To use all of this you need a singular place to reference the window manager implementation. In order to make it possible to reference this functionality globally in the application it is hosted in a single static class – GlobalChildWindowController.

This is really the ‘run-time’ control center of the implementation. Here the different types of windows get defined and used. The ‘hosting’ main form where the tab control is initialize this class.

public static class GlobalChildWindowController
{

private static WindowManagerForTabControl cwm;
public static void SetHostTabControl(TabControl hostTabControl)
{

cwm = new WindowManagerForTabControl(hostTabControl);

}

//Example window type
public static string PersonWindowTypeName = “Person”;
public static UserControlIdentifier PersonWindowType = new UserControlIdentifier()
{

TypeName = PersonWindowTypeName,
UserControl = typeof(PersonEditControl)

};

public static void OpenPersonWindow(int id, bool reUseCurrent)
{

cwm.OpenChildWindow(GlobalChildWindowController.PersonWindowType, id, reUseCurrent);

}

…

internal static void CloseChildWindow(TabPage tabPage)
{

if (tabPage.Tag != null && tabPage.Tag is IChildWindow)
{

cwm.CloseChildWindow((IChildWindow)tabPage.Tag);

}

}

}

With this class you can make the following call any place in the application:

GlobalChildWindowController.OpenPersonWindow(123);

If there is no existing ‘PersonEdit’ window open one will be opened with Id 123. If you need to have a window reused you the overload OpenPersonWindow(123,true).

ChildWindowBase

Now, to develop one of the ‘client windows’ or in this case a UserControl, I created a base class that inherits from IChildWindow and System.Windows.Forms.UserControl to make it easier to implement (otherwise you’d have to redo all the code for all controls you create.

public class ChildWindowBase : UserControl, IChildWindow
{

#region IChildWindow Members
[System.ComponentModel.Browsable(false)]
public int Id { get; set; }
[System.ComponentModel.Browsable(true),
System.ComponentModel.ReadOnly(true)]
public virtual string Type { get { return “Remember to set this value in derived class!”; } set { } }

public virtual string GetWindowTitle()
{

return string.Format(“{0}”, Type);

}
private bool pinned = false;
[System.ComponentModel.Browsable(true)]
public bool Pinned
{

get { return pinned; }
set
{

pinned = value;
RaisePinnedStatusChanged();

}

}

public event PinnedStatusChangedDelegate PinnedStatusChanged;
private void RaisePinnedStatusChanged()
{

if (PinnedStatusChanged != null)

PinnedStatusChanged(this);

}

public event StartCleanupDelegate StartCloseAndCleanup;
private void RaiseWindowClosing()
{

if (StartCloseAndCleanup != null)
{

StartCloseAndCleanup(this);

}

}
public virtual void CloseAndCleanup()
{

RaiseWindowClosing();

}
#endregion

}

To create a user control you do this:

public partial class SomeChild : ChildWindowBase
{

public SomeChild()
{

InitializeComponent();

}

public override string Type
{

get
{

return GlobalChildWindowController.SomeChildTypeName;

}
set
{

base.Type = value;

}

}

}

Conclusion

As mentioned before, this is a work-in-progress development and may not be perfect – if ever 🙂

Try it out. You can get a preview of it here.

The example shows a person view/edit window that can have multiple instances plus some others that only have a single instance. There is also a dialog box example simulating a search function. Additionally I played with a WPF example that is hosted inside an ElementHost container.

string GetWindowTitle();

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Trackbacks and Pingbacks: