Fixing Window leaks and "Class Already Registered" errors

Windows Forms makes it very easy to create a rich UI experience.  But it also makes it easy to leak windows or resources.

For example, say you create a new temporary AppDomain and decide to show some UI in it.  If you close the AppDomain without cleaning up the windows (ie call Dispose on the Controls or close the windows via the UI), then the windows will be orphaned and will not be cleaned up properly.

It is also easy to create new threads with UI.  However, if you shut down the thread that created the UI, any active windows will again be orphaned.  This is because you cannot destroy a window from a thread other than the thread that created the window.

WindowCleanup : A utility to help

Below is a class called "WindowCleanup" along with a Form that displays how it can be used:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.InteropServices;
using System.Threading;

namespace WindowsApplication1
{
/// <summary>
/// Summary description for Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

  public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

   //
// TODO: Add any constructor code after InitializeComponent call
//
}

  /// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

  #region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(56, 152);
this.button1.Name = "button1";
this.button1.TabIndex = 0;
this.button1.Text = "CleanUp";
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// button2
//
this.button2.Location = new System.Drawing.Point(112, 80);
this.button2.Name = "button2";
this.button2.TabIndex = 1;
this.button2.Text = "NewAppDomainWindow";
this.button2.Click += new System.EventHandler(this.button2_Click);
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 266);
this.Controls.Add(this.button2);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);

  }
#endregion

  /// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}

  private void button1_Click(object sender, System.EventArgs e)
{
WindowCleanup.CloseWindows(false, false);
}

  AppDomain domain;

private void button2_Click(object sender, System.EventArgs e)
{
if (domain != null)
{
AppDomain.Unload(domain);
}
domain = AppDomain.CreateDomain("tempdomain");
Form1 domainform = (Form1)domain.CreateInstanceAndUnwrap(typeof(Form1).Assembly.FullName,
typeof(Form1).FullName);

   domainform.Show();
}
}

 class WindowCleanup
{
static bool closeNonManaged = false;
static bool closeOnlyThisThread = false;

  [DllImport("kernel32.dll", CharSet=CharSet.Auto, ExactSpelling=true)]
public static extern int GetCurrentProcessId();
[DllImport("user32.dll", CharSet=CharSet.Auto, ExactSpelling=true)]
public static extern int GetWindowThreadProcessId(HandleRef hWnd, out int lpdwProcessId);
[DllImport("user32.dll", ExactSpelling=true)]
public static extern bool EnumChildWindows(HandleRef hwndParent, EnumWindowCallback lpEnumFunc, HandleRef lParam);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr SendMessage(HandleRef hWnd, int msg, int wParam, int lParam);
[DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool EnumWindows(EnumWindowCallback callback, IntPtr extraData);
[DllImport("kernel32.dll", CharSet=CharSet.Auto, ExactSpelling=true)]
public static extern int GetCurrentThreadId();

  public delegate bool EnumWindowCallback(IntPtr hwnd, IntPtr lParam);
public static HandleRef NullHandleRef;

static public void CloseWindows(bool nonManaged, bool onlyThisThread)
{
WindowCleanup.closeNonManaged = nonManaged;
WindowCleanup.closeOnlyThisThread = onlyThisThread;
EnumWindows(
new EnumWindowCallback(CloseWindowsCallback), IntPtr.Zero);
}

  private static bool CloseWindowsCallback(IntPtr handle, IntPtr extraParameter)
{
int procid1;
int procid2 = GetCurrentProcessId();

int threadid1 = GetWindowThreadProcessId(new HandleRef(null, handle), out procid1);
if (procid1 == procid2)
{
if (closeNonManaged || NativeWindow.FromHandle(handle) != null)
{
if (!closeOnlyThisThread || GetCurrentThreadId() == threadid1)
{
SendMessage(new HandleRef(null, handle), 0x10, 0, 0);
return true;
}
}
EnumChildWindows(new HandleRef(null, handle),
new EnumWindowCallback(CloseWindowsCallback), NullHandleRef);
}

return true;
}
}
}

WindowCleanup has one main static method:

WindowCleanup.CloseWindows(nonManaged, onlyThisThread)

nonManaged:  If this is true, then all windows belonging to the current process will be closed.  If false, then only windows recognized as managed windows in the current appdomain will be closed.

onlyThisThread:  If this is true, then only windows created on the calling thread will be closed.  Use this to clean up any leftover windows before your temporary UI thread terminates.

To see the difference this class makes, run the code above by creating a new C# Windows application and copying this code over Form1.cs.  When you run it, you will see 2 buttons (somewhat randomly placed).

Do this to see an error that occurs when you leak a window:

1.  Run the application

2.  Click the "New AppdomainWindow" button on the first form.  A new form will appear and will have focus.

3.  Go back to the first form and click the button again.  The opened form will disappear and a new form will appear.

4.  Perform step 3 additional times.  Eventually you will get a "ClassAlreadyRegistered" error which occurs because a window was leaked.

 

Now, to see how the class fixes the problem:

1.  Run the application

2.  Click the "New AppdomainWindow" button on the first form.  A new form will appear and will have focus.

3.  Go click the "Cleanup" button on the newly opened form.  That opened form will disappear.

4.  Now click "NewAppdomainForm" again on the first form to show a second form again.

5.  Perform step 3-4 additional times.  You will not get an error because the utility class is cleaning up open windows in the appdomain that will be shut down.

 

Let me know if this helps you at all!