Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#5.0

Thread Apartment Safe Open/Save File Dialogs for C#

5.00/5 (9 votes)
24 Nov 2014CPOL3 min read 46.7K   884  
Using the .NET OpenFileDialog and SaveFileDialog for an application in multithreaded apartment mode (MTA)

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.

Image 1

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.

C#
{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) 

Image 2

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:

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

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

Image 3

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.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace FileDialogTest
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [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:

C#
public virtual DialogResult ShowDialog() 

inside the CFileDlgBase base class.

Here is the implementation of the ShowDialog method inside the CFileOpenDlgThreadApartmentSafe class.

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

  //Create a layout dialog instance on the current thread to align the file dialog Form 
  frmLayout = new Form(); 

  if (this.StartupLocation != null) 
  { //set the hidden layout form to manual form start position 
    frmLayout.StartPosition = FormStartPosition.Manual; 

    //set the location of the form 
    frmLayout.Location = this.StartupLocation; 
    frmLayout.DesktopLocation = this.StartupLocation; 
  } 

  //the layout form is not visible 
  frmLayout.Width = 0; 
  frmLayout.Height = 0; 
  dlgRes = ofd.ShowDialog(frmLayout); 
  
  if (dlgRes == DialogResult.OK) 
    this.FilePath = ofd.FileName; 
}); 

  try 
  { 
    //set STA as the Open file dialog needs it to work 
    theThread.TrySetApartmentState(ApartmentState.STA); 

    //start the thread 
    theThread.Start(); 

    // Wait for thread to get started 
    while (!theThread.IsAlive) { Thread.Sleep(1); } 

    // Wait a tick more (@see: http://scn.sap.com/thread/45710) 
    Thread.Sleep(1); 

    //wait for the dialog thread to finish 
    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

License

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