Click here to Skip to main content
Click here to Skip to main content
Go to top

Extend OpenFileDialog and SaveFileDialog the easy way

, 22 Mar 2012
Rate this:
Please Sign up or sign in to vote.
Customize OpenFileDialog and SaveFileDialog using a User Control
  • Download source files - 278 KB
  • Download release files - 35 KB
  • Table of Contents

    Screenshot - saveas.jpg

    Introduction

    If you used WinForms, chances are that at some point you wanted to extend the OpenFileDialog or SaveFileDialog, but you gave up because there is no easy way to do it, especially if you wanted to add some new graphical elements. Even in the MFC days, such a task was not as daunting as it is for .NET because these classes are sealed, exposing only 16 properties, 5 methods and 3 events that can be used to customize the dialogs. Martin Parry's article on MSDN can give you an insight into how you can customize the OpenFileDialog using the OFNHookProc function and PInvoke. It would look like we have to roll back the clock to early 1990's way of programming and add quite a bit of PInvoke and marshalling to do anything. This is probably enough to give up or look to alternatives like third party libraries. If you are developing for WPF rather than Windows Forms, I suggest that you jump directly to my WPF article on the same topic. However, CastorTiu's article made life A LOT EASIER on those who chose to customize these dialogs using Forms. Using his great work on this topic, I tried to take it even further and make customizing of these two dialogs even more painless. I will focus only on my refactoring and improvements to his original hard work, so if you need details please check CastorTiu's article. While this article uses only C# snippets, I’ve included the equivalent VB.NET code in the downloadable zip file for the VB folks.

    What's New with this Control

    To make extending as easy as possible, I've added some extra properties and events to the base control as well as some design features. Probably the most appreciated property will be the FileDlgType that would allow selecting between OpenFileDialog and SaveFileDialog at design time. The extra properties and events are displayed below:

    Properties Events

    Improvements at Design Time

    It would be nice to have some visual clue about how the control will look like. I did not want to dig too much design time architecture so I used a simple OnPaint override to draw a red line or a dot where the FileDialog touches the extension. Notice that it is painting only in Design mode.

    protected override void OnPaint(PaintEventArgs e)
    {
        if (DesignMode)
        {
            Graphics gr = e.Graphics;
            {
                HatchBrush hb = null;
                Pen p = null;
                try
                {
                    switch (this.FileDlgStartLocation)
                    {
                        case AddonWindowLocation.Right:
                            hb = new System.Drawing.Drawing2D.HatchBrush
                          (HatchStyle.NarrowHorizontal, Color.Black, Color.Red);
                            p = new Pen(hb, 5);
                            gr.DrawLine(p, 0, 0, 0, this.Height);
                            break;
                        case AddonWindowLocation.Bottom:
                            hb = new System.Drawing.Drawing2D.HatchBrush
                          (HatchStyle.NarrowVertical, Color.Black, Color.Red);
                            p = new Pen(hb, 5);
                            gr.DrawLine(p, 0, 0, this.Width, 0);
                            break;
                        case AddonWindowLocation.BottomRight:
                        default:
                            hb = new System.Drawing.Drawing2D.HatchBrush
                        (HatchStyle.Sphere, Color.Black, Color.Red);
                            p = new Pen(hb, 5);
                            gr.DrawLine(p, 0, 0, 4, 4);
                            break;
                    }
                }
                finally
                {
                    if (p != null)
                        p.Dispose();
                    if (hb != null)
                        hb.Dispose();
                }
            }
        }
        base.OnPaint(e);
    }

    How It Works

    CastorTiu's article describes in detail how the control works. I've made some improvements, but the general idea is the same.

    You need to get the dialog handle before it goes modal, so you can 'glue together' the dialog with your own control.

    Here is the flow of initializing of this composite dialog:

    1. Create the dialog using its constructor. At this point there is still no UI, so no Windows messages to catch
    2. Set the properties you want to change at runtime using the virtual OnPrepareMSDialog() for the FileDialog and the Load event for the control itself
    3. Use the DialogResult return and dispose of the control

    Here is what goes on behind the scenes: You start by creating the helper class DialogWrapper with the right FileDialog type as a generic parameter:

    public DialogResult ShowDialog(IWin32Window owner)
    {
        DialogResult returnDialogResult = DialogResult.Cancel;
        if (this.IsDisposed)
            return returnDialogResult;
        if (owner == null || owner.Handle == IntPtr.Zero)
        {
            WindowWrapper wr = new WindowWrapper
    	(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);
            owner = wr;
        }
        OriginalCtrlSize = this.Size;
        _MSdialog = (FileDlgType == 
    	FileDialogType.OpenFileDlg)?new OpenFileDialog()as 
    	FileDialog:new SaveFileDialog() as FileDialog;
        _dlgWrapper = new WholeDialogWrapper(this);
        OnPrepareMSDialog();
        if (!_hasRunInitMSDialog)
            InitMSDialog();
        try
        {
            System.Reflection.PropertyInfo AutoUpgradeInfo = 
    		MSDialog.GetType().GetProperty("AutoUpgradeEnabled");
            if (AutoUpgradeInfo != null)
                AutoUpgradeInfo.SetValue(MSDialog, false, null);
            returnDialogResult = _MSdialog.ShowDialog(owner);
        }
        catch (ObjectDisposedException)
        {
        }
        catch (Exception ex)
        {
            MessageBox.Show("unable to get the modal dialog handle", ex.Message);
        }
        return returnDialogResult;
    }

    When you call ShowDialog public method of your control, the messages start flowing only after you called .NET's Open(Save)FileDialog.ShowDialog(). You have to watch for WM_ACTIVATE, and since an Application filter won't catch it, you have to rely on the parent's window WndProc. Instead of using a dummy form, I found out that a message pump window will do just as fine with less overhead. I created it inside WholeDialogWrapper's constructor calling AssignDummyWindow().

    private void AssignDummyWindow()
    {
        _hDummyWnd = NativeMethods.CreateWindowEx(0, "Message",
            null, WS_VISIBLE, 0, 0, 0, 0,HWND_MESSAGE, NULL, NULL, NULL);
        if (_hDummyWnd == NULL || !NativeMethods.IsWindow(_hDummyWnd))
            throw new ApplicationException("Unable to create a dummy window");
       AssignHandle(_hDummyWnd);
    }

    Since we have this Window that listens to its Children, we will catch the WM_ACTIVATE as below:

    protected override void WndProc(ref Message m)
    {
      switch ((Msg)m.Msg)
      {//.... code omitted
       case Msg.WM_ACTIVATE:
       if (_WatchForActivate && !mIsClosing && m.Msg == (int)Msg.WM_ACTIVATE)
                            //WM_NCACTIVATE works too
       {  //Now the Open/Save Dialog is visible and about to enter the modal loop
          _WatchForActivate = false;
          //Now we save the real dialog window handle
          _FileDialogHandle = m.LParam;
          ReleaseHandle();//release the dummy window
          AssignHandle(_FileDialogHandle);//assign the native open file handle
                                    // to grab the messages
          NativeMethods.GetWindowRect(_FileDialogHandle,
                        ref _CustomControl._DialogWindowRect);
          _CustomControl._FileDialogHandle = _FileDialogHandle;
        }
        break;
       //.... code omitted
      }
      base.WndProc(ref m);
    }

    Once we got the real dialog handle as _FileDialogHandle, we can forget about the dummy Window and start listening to what really matters. Notice how I released the dummy Window handle and assigned the new one. When the same WndProc catches
    the WM_SHOWWINDOW message, we can finally arrange our control and set the parent:

    private void InitControls()
    {
        mInitializated = true;
    
        // Lets get information about the current open dialog
        NativeMethods.GetClientRect(new HandleRef(this,_hFileDialogHandle), 
    				ref _DialogClientRect);
        NativeMethods.GetWindowRect(new HandleRef(this,_hFileDialogHandle), 
    				ref _DialogWindowRect);
    
        // Lets borrow the Handles from the open dialog control
        PopulateWindowsHandlers();
    
        switch (_CustomControl.FileDlgStartLocation)
        {
            case AddonWindowLocation.Right:
                // Now we transfer the control to the open dialog
                _CustomControl.Location = new Point((int)
    		(_DialogClientRect.Width - _CustomControl.Width), 0);
                break;
            case AddonWindowLocation.Bottom:
                // Now we transfer the control to the open dialog
                _CustomControl.Location = new Point(0, 
    		(int)(_DialogClientRect.Height - _CustomControl.Height));
                break;
            case AddonWindowLocation.BottomRight:
                // We don't have to do too much in this case, just the default thing
                _CustomControl.Location = 
    		new Point((int)(_DialogClientRect.Width - _CustomControl.Width), 
    		(int)(_DialogClientRect.Height - _CustomControl.Height));
                break;
        }
        // Everything is ready, now lets change the parent
        NativeMethods.SetParent(new HandleRef(_CustomControl,_CustomControl.Handle), 
    		new HandleRef(this,_hFileDialogHandle));
    
        // Send the control to the back
        // NativeMethods.SetWindowPos(_CustomControl.Handle, 
    	(IntPtr)ZOrderPos.HWND_BOTTOM, 0, 0, 0, 0, UFLAGSZORDER);
        _CustomControl.MSDialog.Disposed += new EventHandler(DialogWrappper_Disposed);
    }

    How the Events are Hooked Up

    You might think that this code is exhaustive relating to the properties and events you can use. Well... not quite! I've added several properties and events to the original work, but you might still want to add more to it.

    Just in the above code snippet, you see how the events from the Open(Save)FileDialog.Dispose are hooked up, but this is an easy one. I'll describe how you can add new events to FileDialogControlBase based on the one I've added as an example.
    There is still another helper class called MSFileDialogWrapper that monitors the Open(Save)FileDialog object through the WndProc as below:

    protected override void WndProc(ref Message m)
    {
        switch ((Msg)m.Msg)
        {
            case Msg.WM_NOTIFY:
                OFNOTIFY ofNotify = (OFNOTIFY)Marshal.PtrToStructure
                        (m.LParam, typeof(OFNOTIFY));
                switch (ofNotify.hdr.code)
                {
                    //.... code omitted
                    case (uint)DialogChangeStatus.CDN_TYPECHANGE:
                        {
                            OPENFILENAME ofn =
                (OPENFILENAME)Marshal.PtrToStructure
                (ofNotify.OpenFileName, typeof(OPENFILENAME));
                            int i = ofn.nFilterIndex;
                            if (_CustomCtrl != null && _filterIndex != i)
                            {
                                _filterIndex = i;
                                _CustomCtrl.OnFilterChanged
                        (this as IWin32Window, i);
                            }
                        }
                        break;
                }
            //.... code omitted
        }
        base.WndProc(ref m);
    }

    Once we marshal the internal pointers into the right structures, we call the OnFilterChanged method of FileDialogControBase object, which is _CustomCtrl in this case. If we dug deeper into this method, we find something as below:

    internal void OnFilterChanged(IWin32Window sender, int index)
    {
        if (EventFilterChanged != null)
            EventFilterChanged(sender, index);
    }

    A quick look at the EventFilterChanged definitions reveals that it is an event.

    public delegate void PathChangedEventHandler(IWin32Window sender,
                                                string filePath);
    public delegate void FilterChangedEventHandler(IWin32Window sender, int index);
    public event PathChangedEventHandler EventFileNameChanged;
    public event PathChangedEventHandler EventFolderNameChanged;
    public event FilterChangedEventHandler EventFilterChanged;
    public event CancelEventHandler EventClosingDialog;

    This makes it very easy to add and remove them the same way you deal with regular WinForms events.

    How to Add New Properties

    To set the properties of the Open(Save)FileDialog object, you have to override the OnPrepareMSDialog() if the design time settings are not right. However changing the appearance of the Open(Save)FileDialog is more elaborate. As an example, I'll show how to change the Text on the Ok button - that's the Save or Open button. We start with
    exposing the property from the FileDialogControlBase and then use PInvoke to set the text as below:

    [DefaultValue("&Open")]
    public string FileDlgOkCaption
    {
        get { return _OKCaption; }
        set { _OKCaption = value; }
    //........................
    
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        if (!DesignMode)
        {
            if (MSDialog != null)
            {
                MSDialog.FileOk += new CancelEventHandler
                        (FileDialogControlBase_ClosingDialog);
                MSDialog.Disposed += new EventHandler
                        (FileDialogControlBase_DialogDisposed);
                MSDialog.HelpRequest += new EventHandler
                        (FileDialogControlBase_HelpRequest);
                NativeMethods.SetWindowText(_dlgWrapper.Handle, _Caption);
                //will work only for open dialog, save dialog will not update
                NativeMethods.SetWindowText(_hOKButton, _OKCaption);
            }
        }
    }

    As you see the comment in the code above, realize that we are dealing with a black box and sometimes we can't know the right way to update. This is one of those cases when we can change it on OpenFileDialog but not on SaveFileDialog. If you wonder how we got the _hOKButton or _dlgWrapper.Handle, you would have to look at FileDialogEnumWindowCallBack that is invoked when the WM_SHOWWINDOW is captured in DialogWrapper's WndProc.

    private bool FileDialogEnumWindowCallBack(IntPtr hwnd, int lParam)
    {
        StringBuilder className = new StringBuilder(256);
        NativeMethods.GetClassName(new HandleRef(this,hwnd), className, className.Capacity);
        int controlID = NativeMethods.GetDlgCtrlID(hwnd);
        WINDOWINFO windowInfo;
        NativeMethods.GetWindowInfo(new HandleRef(this,hwnd), out windowInfo);
        // Dialog Window
        if (className.ToString().StartsWith("#32770"))
        {
            _BaseDialogNative = new MSFileDialogWrapper(_CustomControl);
            _BaseDialogNative.AssignHandle(hwnd);
            return true;
        }
        switch ((ControlsId)controlID)
        {
        //.....code omitted
            case ControlsId.ButtonOk:
                _OKButton = hwnd;
                _OKButtonInfo = windowInfo;
                _CustomControl._hOKButton = hwnd;
                break;
        //.....code omitted
        }
    }

    How to Add the Places Bar to the OpenFileDialog and SaveFiledialog on Windows 2000 and XP

    Until .NET 3.5 introduced FileDialogCustomPlacesCollection for Windows Vista and up, there was no API that I know of to allow you to modify the places bar on the dialog. There is a way to do it on Windows 2000 and XP as described by Dino Esposito in his MSDN article. That requires modifying the registry and it will affect all instances of these dialogs as long as the user is logged on. Since this looks more like a native dialog feature, I’ve used extension methods applied to the base class FileDialog. The static class is shown below and it exposes two public methods to set the places and to restore the registry.

    public static class FileDialogPlaces
    {
        private static readonly string TempKeyName = 
    		"TempPredefKey_" + Guid.NewGuid().ToString();
        private const string Key_PlacesBar = 
    	@"Software\Microsoft\Windows\CurrentVersion\Policies\ComDlg32\PlacesBar";
        private static RegistryKey _fakeKey;
        private static IntPtr _overriddenKey;
        private static object[] m_places;
    
        public static void SetPlaces(this FileDialog fd, object[] places)
        {
            if (fd == null)
                return;
            if (m_places == null)
                m_places = new object[5];
            else
                m_places.Initialize();
    
            if (places != null)
            {
                for (int i = 0; i < m_places.GetLength(0); i++)
                {
                    m_places[i] = places[i];
                }
            }
            if (_fakeKey != null)
                ResetPlaces(fd);
            SetupFakeRegistryTree();
            if (fd != null)
                fd.Disposed += (object sender, EventArgs e) => 
    		{ if (m_places != null && fd != null) ResetPlaces(fd); };
        }
    
        static public void ResetPlaces(this FileDialog fd)
        {
            if (_overriddenKey != IntPtr.Zero)
            {
                ResetRegistry(_overriddenKey);
                _overriddenKey = IntPtr.Zero;
            }
            if (_fakeKey != null)
            {
                _fakeKey.Close();
                _fakeKey = null;
            }
            //delete the key tree
            Registry.CurrentUser.DeleteSubKeyTree(TempKeyName);
            m_places = null;
        }
    
        private static void SetupFakeRegistryTree()
        {
            _fakeKey = Registry.CurrentUser.CreateSubKey(TempKeyName);
            _overriddenKey = InitializeRegistry();
            // write dynamic places here reading from Places
            RegistryKey reg = Registry.CurrentUser.CreateSubKey(Key_PlacesBar);
            for (int i = 0; i < m_places.GetLength(0); i++)
            {
                if (m_places[i] != null)
                {
                    reg.SetValue("Place" + i.ToString(), m_places[i]);
                }
            }
        }
    
        static readonly UIntPtr HKEY_CURRENT_USER = new UIntPtr(0x80000001u);
        private static IntPtr InitializeRegistry()
        {
            IntPtr hkMyCU;
            NativeMethods.RegCreateKeyW(HKEY_CURRENT_USER, TempKeyName, out hkMyCU);
            NativeMethods.RegOverridePredefKey(HKEY_CURRENT_USER, hkMyCU);
            return hkMyCU;
        }
    
        static void ResetRegistry(IntPtr hkMyCU)
        {
            NativeMethods.RegOverridePredefKey(HKEY_CURRENT_USER, IntPtr.Zero);
            NativeMethods.RegCloseKey(hkMyCU);
            return;
        }
    }

    SetPlaces takes an argument as an array of up to five objects that can be numbers as special folders or strings as regular folders. The Disposed event set on the FileDialog makes an automatic call to the ResetPlaces that restores the registry. As a convenience, I’ve included the Places helper enumeration for predefined special folders. Be aware that this could fail on Vista or newer Windows OSes due to UAC.
    If you are running your application on Vista or later, ignore this class and use Microsoft’s FileDialogCustomPlacesCollection class instead, or better yet, use some logic to select the method based on the OS version.

    Using the Control

    Now, let's put it to work. To start using it, you can drop the code in your project or just add a reference to the FileDlgExtenders.dll assembly or to FileDlgExtenders project. If you choose the latter, build the solution before you move forward, because you need the base class at design time. To make things as easy as possible, select 'Add User Control' to your project, than pick 'Inherited User Control' and finally select FileDialogControlBase from the list. As an example, I've added a control called MySaveDialogControl that is just converting images to thumbnails of the desired dimensions, orientation and file format. Next you will probably set the properties and events in design mode. There are 2 ways to display the control.

    Display it by calling the regular method ShowDialog

    To set up FileDialog's data at runtime, override the virtual method OnPrepareMSDialog() in your subclass. You should call base.OnPrepareMSDialog() firstly, so your own changes won't be wiped away. Below is an example of how I change FileDialog's properties at runtime in my derived control.

    protected override void OnPrepareMSDialog()
    {
        base.FileDlgInitialDirectory = Environment.GetFolderPath
    				(Environment.SpecialFolder.MyPictures);
        if (Environment.OSVersion.Version.Major < 6)
        	MSDialog.SetPlaces( new object[] { @"c:\", 
    	(int)Places.MyComputer, (int)Places.Favorites, (int)Places.Printers, 
    	(int)Places.Fonts, });
        base.OnPrepareMSDialog();
    }

    A similar precaution is needed if you choose to override OnLoad. Always call base.OnLoad, otherwise the Load event won't get called. As a tip, if Visual Studio can't render the new control, clean the solution and rebuild it again. If it still does not work, you might have to manually purge the bin folders, and check what objects you initialize at Design time.
    Finally, here is the caller displaying it:

    using (MySaveDialogControl saveDialog = new MySaveDialogControl(lblFilePath.Text, this))
    {
        if (saveDialog.ShowDialog(this) == DialogResult.OK)
        {
            lblFilePath.Text = saveDialog.MSDialog.FileName;
        }
    }

    Display it by Calling the Extension Method ShowDialog of Extensions Class

    Some of you might like more the syntactical sugar offered by the extension method ShowDialog:

    public static class Extensions
    {
        public static DialogResult ShowDialog
    	(this FileDialog fdlg, FileDialogControlBase ctrl, IWin32Window owner)
       {
            ctrl.FileDlgType =(fdlg is SaveFileDialog)?
    		FileDialogType.SaveFileDlg: FileDialogType.OpenFileDlg;
            if (ctrl.ShowDialogExt(fdlg, owner) == DialogResult.OK)
                return DialogResult.OK;
            else
                return DialogResult.Ignore;
        }
    }

    You won’t have to care about overriding OnPrepareMSDialog() anymore, but you will have to set the FileDialog members programmatically at runtime in the caller code.

    using (MyOpenFileDialogControl openDialogCtrl = new MyOpenFileDialogControl())
    {
        openDialogCtrl.FileDlgInitialDirectory = 
    	Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
        OpenFileDialog openDialog = new OpenFileDialog();
        openDialog.InitialDirectory = Environment.GetFolderPath
    		(Environment.SpecialFolder.MyPictures);
        openDialog.AddExtension = true;
        openDialog.Filter = "Image Files(*.bmp)|*.bmp 
    	|Image Files(*.JPG)|*.JPG|Image Files(*.jpeg)|*.jpeg
    	|Image Files(*.GIF)|*.GIF|Image Files(*.emf)|*emf.|
    	Image Files(*.ico)|*.ico|Image Files(*.png)|*.png|
    	Image Files(*.tif)|*.tif|Image Files(*.wmf)|*.wmf|Image Files(*.exif)|*.exif";
        openDialog.FilterIndex = 2;
        openDialog.CheckFileExists = true;
        openDialog.DefaultExt = "jpg";
        openDialog.FileName = "Select Picture";
        openDialog.DereferenceLinks = true;
        if (Environment.OSVersion.Version.Major < 6)
        	openDialog.SetPlaces(new object[] { @"c:\", 
    	(int)Places.MyComputer, (int)Places.Favorites, 
    	(int)Places.Printers, (int)Places.Fonts, });
        if (openDialog.ShowDialog(openDialogCtrl, this) == DialogResult.OK)
        {
            lblFilePath.Text = openDialog.FileName;
        }
    }

    History

    • This is version 1.0 if you disregard the work it's based on. For me, it works fine on 32 bit Windows XP SP3. I hope you'll enjoy it.
    • Version 1.1 fixes a bug associated with the destruction of the file list and keeps the handle and the view mode up to date. I've added a new design time property to enable/disable the OK button called 'FileDlgEnableOkBtn'.
      Below is an example of how this property can be used based on the conversion result:
      private void MySaveDialogControl_FilterChanged
                      (IWin32Window sender, int index)
      {
          FileDlgEnableOkBtn = GetFormatFromIndex(index);
      }
    • Version 1.2 added several fixes suggested by viewers who left feedback.
      Here are the most important changes as I see them:
      • John Horigan: set the 'AutoUpgradeEnabled' property for Vista/7 to false
      • LETRESTE Bruno: improved the autosize when showing the dialog
      • The solution has been converted to Visual Studio 2008. The old source code for VS 2005 is available in the customFileDialog_old.zip
      • If you still use VS 2005, just copy the new *.cs files over the old ones and open the solution/project. It should work.
      • I also used an automated tool to create a VB version for the same solution.
      • The VB source code is included too and it seems to work.
    • Version 1.3 has fixed more issues, thanks to viewers like beautyod
    • I’ve also added support for the places bar as a new feature targeted at windows 2000 and XP.
    • There are solution files for VS 2008(.NET 3.5) and 2010(.NET 4.0) for both VB and C#. The previous source code is available in the embedded CustomFileDialog_src_old.zip archive.
    • Version 1.4 is incorporating more fixes related to Windows 7, 64 bit, etc, thanks to the comments provided by Phil Atkin, John Simmons / outlaw programmer, neyerMat, kore_sar and others.

    License

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

    Share

    About the Author

    dmihailescu
    Software Developer (Senior)
    United States United States
    Decebal Mihailescu is a software engineer with interest in .Net, C# and C++.

    Comments and Discussions

     
    QuestionHow to close the dialog programatically PinmemberPhil Jollans4-Oct-12 22:16 
    AnswerRe: How to close the dialog programatically Pinmemberdmihailescu5-Oct-12 15:46 
    Call the OK event.
    GeneralRe: How to close the dialog programatically PinmemberPhil Jollans8-Oct-12 21:40 
    GeneralRe: How to close the dialog programatically PinmemberPhil Jollans7-Nov-12 5:40 
    GeneralRe: How to close the dialog programatically PinmemberPhil Jollans8-Nov-12 22:14 

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

    | Advertise | Privacy | Mobile
    Web01 | 2.8.140926.1 | Last Updated 22 Mar 2012
    Article Copyright 2007 by dmihailescu
    Everything else Copyright © CodeProject, 1999-2014
    Terms of Service
    Layout: fixed | fluid