Introduction
At work, I recently had to develop a tool that creates several modeless windows. These modeless windows present training data for individuals, test metrics for individuals, and available course data. While modeless windows really aren't new or exciting, I had the added consideration of not allowing the user to select the same user twice and have two different windows (the data is static, so there's no point in having two windows showing the same data. This requirement made it impossible to simply show modeless windows, and forced me to come up with a modeless window manager, and this is the subject of this article.
The article presents information in the following order: introduction, usage, and code description. This is beneficial to people with diminished attention spans that don't want to know how the code works, but want the code anyway, and without having to suffer the burden and drudgery of technical knowledge.
Features
The following capabilities are implemented in the ModelessWindowMgr
class:
- If the window's
Owner
property isn't specifically set, the MainWindow
is used as the Owner.
- Automatically handles the
Owner_Closing
event, so that when the owner window closes, all modeless windows created by the window will also automatically be closed. The Owner_Closing
event handler is virtual, so you can override it when necessary.
- The window manager can be configured to replace an existing window with the same identifying property value.
- The window manager can be configured to allow duplicate instances of windows with the same identifying property value.
- You can use either the provided id properties in the
IModelessWindow
interface, or one of the many Window
properties that might provide a method for uniquely identifying a given window (Title, Name, Tag, etc).
- You can prevent a given modeless window from being used as a modal form.
Usage
Here are the steps to minimally implement the ModelessWindowMgr
. Sample code provided below is only here to illustrate what needs to be done, and is an abbreviated version of what is in the sample code. There is no XAML involved in the actual implementation , other than when you create your modeless windows, but even they don't use any if the window manager features.
0) Add the ModelessWindowMgr
file to your project in any way that suits your project. In my case, I have a WpfCommon
assembly where all of my general WPF stuff lives.
1) When you create a window that is intended to be put in the window manager, you must inherit and implement the IModelessWindow
interface. In the example shown below, I also have a base window class that inherits/implements INotifyPropertyChanged
(NotifiableWindow
), so I inherit from that window class, and the IModelessWindow
interface. Take note of the HandleModeless()
method - this method is called by the window manager to add the Owner_Closing event handler for you.
I strongly recommend that you adopt a similar base class strategy. This will allow you to essentially forget about the window-specific implementation and just get on with your coding.
public class ModelessWindowBase : NotifiableWindow, IModelessWindow
{
#region IModelessWindow implementation
// the two properties that must be included in your window class
public string IDString { get; set; }
public int IDInt { get; set; }
public bool IsModal()
{
return ((bool)(typeof(Window).GetField("_showingAsDialog",
BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this)));
}
public void HandleModeless(Window owner)
{
// we can't user the Window.Owner property because doing so causes the
// modeless window to always be on top of its owner. This is a "bad thing"
// (TM). Therefore, it's up to the developer to supply the owner window
// when he adds the modeless window to the manager.
owner.Closing += this.Owner_Closing;
}
public virtual void Owner_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (!this.IsModal())
{
this.Close();
}
}
#endregion IModelessWindow implementation
public TestWindowBase():this(string.Empty, 0)
{
}
public TestWindowBase(string idString, int idInt=0) : base()
{
// populate the id properties
this.IDString = idString;
this.IDInt = idInt;
}
}
2) Create a window that inherits from your base class:
public partial class Window1 : ModelessWindowBase
{
public Window1()
{
this.InitializeComponent();
}
public Window1(string id):base(id)
{
this.InitializeComponent();
}
}
And change your xaml to match the base class:
local:ModelessWindowBase x:Class="WpfModelessWindowMgr.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
...
xmlns:local="clr-namespace:WpfModelessWindowMgr"
Title="Window1" Height="100" Width="600"
x:Name="ModelessWindow1" Tag="1">
...
</local:ModelessWindowBase>
3) Add the class manager to your MainWindow
object.
public partial class MainWindow : NotifiableWindow
{
private ModelessWindowMgr manager;
public ModelessWindowMgr Manager
{ get { return this.manager; }
set { if (value != this.manager) { this.manager = value; } } }
private string Window1Guid { get; set; }
public MainWindow()
{
this.InitializeComponent();
this.DataContext = this;
string idProperty = "IDString";
bool replaceIfExists = false;
bool allowDuplicates = false;
this.Manager = new ModelessWindowMgr("IDString",
replaceIfExists,
allowDuplicates);
}
}
4) Add code to create instantiate your modeless window (created in step 2 above). If you don't want the window to display immediately, add false as a 2nd parameter to the Add
method.
Window1 form = new Window1(this.Window1Guid);
this.Manager.Add(form);
That's pretty much it. You now have a fully functional modeless window manager implementation. Since the windows that inherit from IModelessWindow
automatically hook the Owner_Closing
event, any modeless windows (added to the window manager) that are still open when you close the main window will be automatically cleaned up.
The Code
This section will only describe the ModelessWindowMgr
class and the implementation of the IModelessWindow
interface.
IModelessWindow Interface
The IModelessWindow
interface supports two types of id properties, and a few methods necessary to guarantee that a given window will work within the ModelessWindowMgr
ecosystem. The id properties allow a common method for identifying a given window in the window list maintained by the window manager. I figured it made sense to provide a choice between using an integer and a string. This id value is probably best assigned either with a Window constructor parameter, or some other mechanism within the window itself (such as the Title
, Name
, or Tag
property in the window's XAML).
The actual implementation of the interface also involves a handful of methods. The suggested implementation (which includes the code below) can be found as a comment in IModelessWindow
.cs.
public bool IsModal()
{
return ((bool)(typeof(Window).GetField("_showingAsDialog",
BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this)));
}
public void HandleModeless()
{
owner.Closing += this.Owner_Closing;
}
public virtual void Owner_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (!this.IsModal())
{
this.Close();
}
}
One more aspect of the interface implementation is the ability to actually prevent the window from being used as a modal window. By default, the PreventModal
property is set to false, but it's there if you need/want it.
public new bool? ShowDialog()
{
if (this.PreventModal)
{
throw new InvalidOperationException("Can't treat a this window like a modal window.");
}
return base.ShowDialog();
}
ModelessWindowManager Class
The basic functions of the ModelessWindowMgr
is adding, displaying, and removing windows.
Adding/Displaying Windows
Adding and displaying windows is handled by a pair of methods. The Add
method allows you to specify the instantiated (but not shown) modeless window, and you can optionally specify whether or not to show the window immediately (the default is to show the window immediately). Instead of providing narrative and code, I'll just show you the code which I think is commented sufficiently.
public void Add(IModelessWindow window, bool showNow=true)
{
if (owner == null)
{
throw new ArgumentNullException("owner");
}
if (window == null)
{
throw new ArgumentNullException("window");
}
if (window is Window)
{
IModelessWindow existingWindow = null;
bool addIt = false;
if (this.AllowDuplicates)
{
addIt = true;
}
else
{
existingWindow = this.Find(window);
if (existingWindow != null)
{
((Window)window).Close();
window = null;
if (this.ReplaceIfOpen)
{
((Window)(existingWindow)).Close();
this.Windows.Remove(existingWindow);
existingWindow = null;
addIt = true;
}
}
else
{
addIt = true;
}
}
if (addIt)
{
window.HandleModeless(owner);
this.Windows.Add(window);
if (showNow)
{
((Window)(window)).Show();
}
}
else
{
Window wnd = ((Window)(existingWindow));
if (wnd.Visibility == Visibility.Collapsed)
{
wnd.Show();
}
if (wnd.WindowState == WindowState.Minimized)
{
wnd.WindowState = WindowState.Normal;
}
wnd.Focus();
}
}
}
protected IModelessWindow Find(IModelessWindow window)
{
Window foundAsWindow = null;
Window windowAsWindow = ((Window)window);
IModelessWindow found = null;
try
{
if (new string[] { "IDString", "IDInt" }.Contains(this.IDProperty))
{
string value = typeof(IModelessWindow).GetProperty(this.IDProperty).GetValue(window).ToString();
found = this.Windows.FirstOrDefault(x => x.IDString == value || x.IDInt.ToString() == value);
}
else
{
object windowPropertyValue = typeof(Window).GetProperty(IDProperty).GetValue(window);
found = this.Windows.FirstOrDefault(x => typeof(Window).GetProperty(IDProperty).GetValue(x).Equals(windowPropertyValue));
}
foundAsWindow = ((Window)found);
}
catch (Exception ex)
{
if (ex != null) { }
}
return found;
}
Removing Windows
Windows can be removed by id property value, or by underlying window type, and they can be removed one at a time (first or last) or all at once. All of these methods get a list of windows to remove (even if it's only to remove one window).
public void RemoveAll()
public void RemoveFirstWithID(object id)
public void RemoveLastWithID(object id)
public void RemoveAllWithID(object id)
public void RemoveFirstWindowOfType(Type windowType)
public void RemoveLastWindowOfType(Type windowType)
public void RemoveAllWindowsOfType(Type windowType)
This allows the actual removal code exist in a simgle common method, which is easier to troubleshoot. That method looks like this:
protected void RemoveThese(List<IModelessWindow> windows)
{
while (windows.Count > 0)
{
IModelessWindow window = windows[0];
((Window)window).Close();
this.Windows.Remove(window);
windows.Remove(window);
}
}
Points of Interest
If you want the window manager accessible throughout your application, I recommend that you use your favorite method for doing so. I typically do one of these:
- Create a static global class, and instantiate it there.
- Create a singleton that represents the
ModelessWindowMgr
object.
I also discovered that if you set the Owner
property on a Window
, the window you're creating will always stay on top of the owner window - not a desireable effect. This caused me to require the owner window to be specifed when you try to add the window to the window manager.
Finally, it should be beyond trivial to convert this code to .Net Core. Have a ball.
Standard JSOP Disclaimer
This is the latest in my series of articles that describe solutions to real-world programming problems. There is no theory pontification, no what-ifs, and no hypothetical claptrap discussed here. If you're looking for something that's ground-breaking, world-shaking or even close to cutting edge, I recommend that you seek out other reading material (the .Net Core fan boys seem to be pretty proud of themselves). I'm not known to be one that forges a path for others to follow (indeed, the only example I should serve is of what *not* to do), and will never claim that mine is the "one true way" (except where the evil that *is* Entity Framework is concerned). If this article doesn't spin your tea cups, by all means, move along, happy trails, and from the very depths of my Texas soul (with all the love and respect I can possibly muster), AMF.
History
- 2021.03.02 - corrected the code snippet for the IModelessWindow implementation (the actual code in the download is correct, but I forgot to update the article text).
- 2021.03.01 - Initial publication.