Download FileDialogsThreadAppartmentSafe_v1.zip
If you are using C# for your desktop development projects, you might know the power of the default C# wrapper classes for the Win32 Open and Save file dialogs. They are easy to use and you will always have the correct style of Windows.
But there are also some issues, some of them are discussed in this article.
Background
Once you are developing bigger applications, you might notice that once you have set the caller thread apartment state to MTA, the default OpenFileDialog
and SaveFileDialog
dialogs will not work anymore. You will receive the following exception once you have called the ShowDialog
method of the instance.
{System.Threading.ThreadStateException: Current thread must be set to
single thread apartment (STA) mode before OLE calls can be made.
Ensure that your Main function has STAThreadAttribute marked on it.
This exception is only raised if a debugger is attached to the process.
at System.Windows.Forms.FileDialog.RunDialog(IntPtr hWndOwner)
at System.Windows.Forms.CommonDialog.ShowDialog(IWin32Window owner)
This problem can be solved easily by creating a new thread in STA mode which calls the open or save file dialogs. But then, the next problem pops up if your application should work on multiple display devices. You will notice that you cannot set the parent of the open file dialog like you would with a common winforms form instance.
So, how to solve this one? Well, the first thing that was in my mind was the usage of the default Win32
methods to set the locations for these dialogs by using the following PInvokes:
[DllImport("User32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool GetWindowRect(IntPtr handle, ref RECT r);
[DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetWindowPos(IntPtr hWnd,
IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
The position of the window could be set on any Win32
dialog instance. But once you do take a closer look at the different members and attributes of the Open/Save file dialogs, you will notice that these dialogs do not have the Handle
member which is the IntPtr
to the underlying Win32
dialog instance like within the common System.Windows.Forms
dialogs.
But using static PInvoke
methods is not really .NET object oriented, right? And if your application is a MDI application, things do get a bit messy because you need the IntPtr
to the dialogs for all of these dialog instances to use the Win32 PInvokes
methods.
So I decided to create two classes, CFileOpenDlgThreadApartmentSafe
and CFileSaveDlgThreadApartmentSafe
using a base class named CFileDlgBase
with common methods and members for file dialogs.
My goal was to have dialog classes:
- With attributes comparable to the default .NET dialogs
- Callable from STA and MTA threaded callers
- With modal behavior like the original dialogs
- Not using
static PInvoke
methods
These classes can be found within the FileDialogsThreadAppartmentSafe
assembly.
How to Use the Code
Reference the assembly FileDialogsThreadAppartmentSafe.dll within your project and use the classes in the following way:
CFileOpenDlgThreadApartmentSafe dlg = new CFileOpenDlgThreadApartmentSafe();
dlg.Filter = "Text file (*.txt)|*.txt";
dlg.DefaultExt = "txt";
Point ptStartLocation = new Point(this.Location.X, this.Location.Y);
dlg.StartupLocation = ptStartLocation;
DialogResult res = dlg.ShowDialog();
if (res != System.Windows.Forms.DialogResult.OK)
return;
MessageBox.Show(string.Format("Open file {0}", dlg.FilePath));
The second project is the example where both dialogs are used as well as the original base implementations.
Within the Program.cs file in line 13, you can see that the main method is marked as [MTAThread]
. That’s why you will receive the exception shown above when clicking on the buttons marked as Not Safe.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace FileDialogTest
{
static class Program
{
[MTAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
Points of Interest
The most interesting part of the implementation is the call of the ShowDialog()
method of both classes. This method is defined as:
public virtual DialogResult ShowDialog()
inside the CFileDlgBase
base class.
Here is the implementation of the ShowDialog
method inside the CFileOpenDlgThreadApartmentSafe
class.
public override DialogResult ShowDialog()
{
DialogResult dlgRes = DialogResult.Cancel;
Thread theThread = new Thread((ThreadStart)delegate
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Multiselect = false;
ofd.RestoreDirectory = true;
if (!string.IsNullOrEmpty(this.FilePath))
ofd.FileName = this.FilePath;
if (!string.IsNullOrEmpty(this.Filter))
ofd.Filter = this.Filter;
if (!string.IsNullOrEmpty(this.DefaultExt))
ofd.DefaultExt = this.DefaultExt;
if (!string.IsNullOrEmpty(this.Title))
ofd.Title = this.Title;
if (!string.IsNullOrEmpty(this.InitialDirectory))
ofd.InitialDirectory = this.InitialDirectory;
frmLayout = new Form();
if (this.StartupLocation != null)
{
frmLayout.StartPosition = FormStartPosition.Manual;
frmLayout.Location = this.StartupLocation;
frmLayout.DesktopLocation = this.StartupLocation;
}
frmLayout.Width = 0;
frmLayout.Height = 0;
dlgRes = ofd.ShowDialog(frmLayout);
if (dlgRes == DialogResult.OK)
this.FilePath = ofd.FileName;
});
try
{
theThread.TrySetApartmentState(ApartmentState.STA);
theThread.Start();
while (!theThread.IsAlive) { Thread.Sleep(1); }
Thread.Sleep(1);
theThread.Join();
DialogSuccess = true;
}
catch (Exception err)
{
DialogSuccess = false;
}
return (dlgRes);
}
The method starts a new thread in Single Thread Apartment mode and creates an invisible dialog instance used as parent for the Win32
file dialogs. This way, we do not need to work on IntPtr
's or on instances created on different threads. After the dialog is shown, the method waits for the dialog thread to finish with the threads Join
method. The blocking of the caller thread produces the modal dialog behavior even if the real file dialog is created on a new thread instance.
Links and References