Click here to Skip to main content
15,881,715 members
Articles / Desktop Programming / WPF

WPF - Modeless Window Manager

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
1 Mar 2021CPOL6 min read 6.6K   179   11  
For those times when you need more control over when a modeless window is created/displayed

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.

Modeless window manager screen shot
 

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.

XML
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:

C#
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:

C#
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.

C#
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.

C#
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.

C#
/// <summary>
/// Determine if the window is modal
/// </summary>
/// <returns></returns>
public bool IsModal()
{
    // uses a private Window property to determine if this window is modal
    return ((bool)(typeof(Window).GetField("_showingAsDialog", 
            BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this)));
}

/// <summary>
/// Adds an event handler for the Owner window's Closing event.
/// </summary>
public void HandleModeless()
{
    // 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;
}

/// <summary>
/// The closing event for this window's owner.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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.

C#
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.

C#
/// <summary>
/// Add/activate the specified window
/// </summary>
/// <param name="owner">The owner window (cannot be null)</param>
/// <param name="window">The IModelessWindow window (cannot be null)</param>
/// <param name="showNow">Show the window now, or simply add it to manager</param>
public void Add(IModelessWindow window, bool showNow=true)
{
    // sanity checks
    if (owner == null)
    {
        throw new ArgumentNullException("owner");
    }
    if (window == null)
    {
        throw new ArgumentNullException("window");
    }

    // first, make sure the "window" is really a window (to keep the developer from trying 
    // to catch us asleep at the wheel)
    if (window is Window)
    {
        IModelessWindow existingWindow = null;
        bool            addIt          = false;

        // if we're allowing duplicates, we can skip all this and just get down to 
        // showing the window.
        if (this.AllowDuplicates)
        {
            addIt = true;
        }
        else
        {
            // see if it's already in the list
            existingWindow       = this.Find(window);
            // if it is already in the list
            if (existingWindow != null)
            {
                // go ahead and close the new window and set it to null. There's 
                // no point in keeping it around.
                ((Window)window).Close();
                window = null;

                // if we need to replace it, eradicate the existing instance first
                if (this.ReplaceIfOpen)
                {
                    ((Window)(existingWindow)).Close();
                    this.Windows.Remove(existingWindow);
                    existingWindow = null;
                    addIt = true;
                }
            }
            else
            {
                addIt = true;
            }
        }
        // add or focus the window
        if (addIt)
        {
            // make sure we hook the Owner_Closing event
            window.HandleModeless(owner);
            // and add the window to the list
            this.Windows.Add(window);
            // if we want immediate gratification, show the 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();
        }
    }
}

/// <summary>
/// Find the window in the manager's window list that matches <br/>
/// the ID property value of the specified window.
/// </summary>
/// <param name="window">The window to find</param>
/// <returns>The matching window if found, or null</returns>
protected IModelessWindow Find(IModelessWindow window)
{
    // "The relationship is new. Let's not sully it with an exchange of saliva..."
    // George Takei, Big Bang Theory, S4, E4

    Window foundAsWindow = null;
    Window windowAsWindow = ((Window)window);

    // find a window with the same id property value. We can check both the string and int 
    // id properties because we converted the value we're looking for into a string. We 
    // also check to make sure the window is actually modeless
    IModelessWindow found = null;
    try
    {
        // if the id property is one of the two defined in the IModelessWindow interface
        if (new string[] { "IDString", "IDInt" }.Contains(this.IDProperty))
        {
            // get the named property's value as a string
            string value = typeof(IModelessWindow).GetProperty(this.IDProperty).GetValue(window).ToString();

            // and find the first window that has the same value for the id property
            found = this.Windows.FirstOrDefault(x => x.IDString == value || x.IDInt.ToString() == value);
        }
        else
        {
            // get the value of the specified Window property, could be Title, Tag, or some 
            // other property that could be used to uniquely identify a window
            object windowPropertyValue = typeof(Window).GetProperty(IDProperty).GetValue(window);

            // and find the first window that has the same value for the specified property
            found = this.Windows.FirstOrDefault(x => typeof(Window).GetProperty(IDProperty).GetValue(x).Equals(windowPropertyValue));
        }
        foundAsWindow = ((Window)found);
    }
    catch (Exception ex)
    { 
        // avoid the compiler warning while allowing us to examine the exception in the 
        // debugger if necessary
        if (ex != null) { }
        // We don't want to do anything but eat the exception. This means if there's an 
        // exception, this method will return null, thus guaranteeing that the window 
        // will be shown.
    }
    // return the result (null or the found window)
    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).

C#
/// <summary>
/// Removes all windows from the windows manager
/// </summary>
public void RemoveAll()

/// <summary>
/// Remove the first window with the specified id value from the windows manager
/// </summary>
/// <param name="id">The value of the IDProperty property</param>
public void RemoveFirstWithID(object id)

/// <summary>
/// Remove the last window with the specified id value from the windows manager
/// </summary>
/// <param name="id">The value of the IDProperty property</param>
public void RemoveLastWithID(object id)

/// <summary>
/// Remove the all windows with the specified id value from the windows manager
/// </summary>
/// <param name="id">The value of the IDProperty property</param>
public void RemoveAllWithID(object id)

/// <summary>
/// Remove the first window of the specified type from the windows manager
/// </summary>
/// <param name="windowType">The underlying window type</param>
public void RemoveFirstWindowOfType(Type windowType)

/// <summary>
/// Remove the first window of the specified type from the windows manager
/// </summary>
/// <param name="windowType">The underlying window type</param>
public void RemoveLastWindowOfType(Type windowType)

/// <summary>
/// Remove the first window of the specified type from the windows manager
/// </summary>
/// <param name="windowType">The underlying window type</param>
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:

C#
/// <summary>
/// Remove specified windowswindows manager
/// </summary>
/// <param name="windows">The list of found windows to remove</param>
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.
     

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Paddedwall Software
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.

My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
-- There are no messages in this forum --