Click here to Skip to main content
Click here to Skip to main content

C# does Shell, Part 2

By , 12 Jan 2010
Rate this:
Please Sign up or sign in to vote.
Sample Image - csdoesshell2.jpg

Introduction

This article continues to explore how to use the shell from C#. In this article, I will still not touch in 'extending the shell', cause there is some functionality the shell gives us that I want to review first. Unlike Part 1, which was full of basic details, this article is quite simple, but I do trust you read the first article. So in case I see something I've explained in part 1, I won't hesitate to mention it and not explain it.

Again I suggest the following articles from the MSDN will be read first, the article does not suppose to explain all the shell functionality, others have done this already, the article comes to explain how to do it in C#. The following links are suggested reading material:

So, in this article I'll discuss launching application in C# with the shell, doing file operations in C# using the shell, adding files to the Recent Document list in C# using the shell and doing some printer operations in C# using the shell.

But, the main thing I'll explain is why don't I use the normal C# way to do all this.

Note, the code in this article will use and extend the code written in Part 1, I mean the ShellLib class library will grow a bit.

Main Goals..

So, why should I use the shell way to do stuff when C# offers me a simpler way? Well, the problem is that C# does not give me all the Windows options I can get. I don't blame them, if you think about it, if C# and .NET in general are supposed to be platform independent languages, they can't give support for specific platform options.

Let's review our main goals of this article:

  1. Launching applications with different kind of 'verb's (open, edit, print)
  2. Doing file operation with the shell support (Recycle bin, progress bar)
  3. Adding files to the Recent Document list
  4. Doing printer management operations

Enough wasting time, let's get to work...

Section 1: Launching Applications

So, what is our objective in this section? To provide a simple class that lets us launch an application according to their file types (paint for BMP, media player for wave, etc.). This class should also support verbs. Verbs are operations that can be done on a file. Each file can have different operations, but most files have the 'open' verb, 'edit' verb, 'properties' verb and more.

What do we need to do to perform these verbs on a file? We need to use the function ShellExecute, and pass it the parameters like file name and what operation we want to perform on the file.

So first we need to declare the ShellExecute API, this is done as follows:

// Performs an operation on a specified file.
[DllImport("shell32.dll")]
public static extern IntPtr ShellExecute(
    IntPtr hwnd,               // Handle to a parent window.
    [MarshalAs(UnmanagedType.LPTStr)]
    String lpOperation,   // Pointer to a null-terminated string, referred to in 
                          // this case as a verb, that specifies the action to 
                          // be performed.
    [MarshalAs(UnmanagedType.LPTStr)]
    String lpFile,        // Pointer to a null-terminated string that specifies 
                          // the file or object on which to execute the specified 
                          // verb.
    [MarshalAs(UnmanagedType.LPTStr)]
    String lpParameters,  // If the lpFile parameter specifies an executable file, 
                          // lpParameters is a pointer to a null-terminated string 
                          // that specifies the parameters to be passed to the 
                          // application.
    [MarshalAs(UnmanagedType.LPTStr)]
    String lpDirectory,   // Pointer to a null-terminated string that specifies
                          // the default directory. 
    Int32 nShowCmd);      // Flags that specify how an application is to be
                          // displayed when it is opened.

Here is an example for using this function:

int iRetVal;
iRetVal = (int)ShellLib.ShellApi.ShellExecute(
    this.Handle,
    "edit",
    @"c:\windows\Greenstone.bmp",
    "",
    Application.StartupPath,
    (int)ShellLib.ShellApi.ShowWindowCommands.SW_SHOWNORMAL);

I've added a small class that lets you use this function easily, here is the implementation:

public class ShellExecute
{
    // Common verbs
    public const string OpenFile         = "open";

                        // Opens the file specified by the 
                        // lpFile parameter. The file can 
                        // be an executable file, a document 
                        // file, or a folder.

    public const string EditFile         = "edit";        

                        // Launches an editor and opens the 
                        // document for editing. If lpFile 
                        // is not a document file, the 
                        // function will fail.

    public const string ExploreFolder    = "explore";     

                        // Explores the folder specified by 
                        // lpFile.

    public const string FindInFolder     = "find";        

                        // Initiates a search starting from 
                        // the specified directory.

    public const string PrintFile        = "print";       

                        // Prints the document file specified 
                        // by lpFile. If lpFile is not a 
                        // document file, the function will 
                        // fail.
    
    // properties
    public IntPtr OwnerHandle;     // Handle to the owner window
    public string Verb;            // The requested operation to make on the
                                   // file
    public string Path;            // String that specifies the file or 
                                   // object on which to execute the 
                                   // specified verb.
    public string Parameters;      // String that specifies the parameters to 
                                   // be passed to the application.
    public string WorkingFolder;   // specifies the default directory

    public ShellApi.ShowWindowCommands ShowMode;      

                                   // Flags that specify how an application 
                                   // is to be displayed when it is opened.
 
    public ShellExecute()
    {
        // Set default values
        OwnerHandle = IntPtr.Zero;
        Verb = OpenFile;
        Path = "";
        Parameters = "";
        WorkingFolder = "";
        ShowMode = ShellApi.ShowWindowCommands.SW_SHOWNORMAL;
    }
 
    public bool Execute()
    {
        int iRetVal;
        iRetVal = (int)ShellLib.ShellApi.ShellExecute(
            OwnerHandle,
            Verb,
            Path,
            Parameters,
            WorkingFolder,
            (int)ShowMode);
 
        return (iRetVal > 32) ? true : false;
    }
}

And here is how you use the class:

ShellLib.ShellExecute shellExecute = new ShellLib.ShellExecute();
shellExecute.Verb = ShellLib.ShellExecute.EditFile;
shellExecute.Path = @"c:\windows\Coffee Bean.bmp";
shellExecute.Execute();

Note: Some of this functionality can be achieved using the Process and ProcessStartInfo classes. But our goal in this article is using the shell functions, which allows us better flexibility.

Section 2: Doing File Operations

What do these file operations do? What is the difference between the way we will copy a file in the section and the normal way? Well the normal way is usually implemented using the API functions: CopyFile, MoveFile, DeleteFile that belong to the File Storage API set, these functions will do the job, but the shell function can give you also the shell support for this file, meaning with the shell function you can see the progress dialog when you do a copy operation, you can have the deleted files moved to the recycle bin. You can make simple undo of your operations. And you get as a bonus all the nice dialogs that appear when you do these operations through the explorer.

So, how it is done? Using the SHFileOperation API function, this operation gets a struct that contains all the info the operation needs, including the source and destination, special flags and more. The C# declaration of this function is:

// Copies, moves, renames, or deletes a file system object. 
[DllImport("shell32.dll" , CharSet = CharSet.Unicode)]
public static extern Int32 SHFileOperation(
    ref SHFILEOPSTRUCT lpFileOp);       // Address of an SHFILEOPSTRUCT 
                // structure that contains information this function needs 
                // to carry out the specified operation. This parameter must 
                // contain a valid value that is not NULL. You are 
                // responsible for validating the value. If you do not 
                // validate it, you will experience unexpected results.

As you can see from the function declaration, it expects a structure called SHFILEOPSTRUCT, here is its definition:

// Contains information that the SHFileOperation function uses to perform 
// file operations. 
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct SHFILEOPSTRUCT
{
    public IntPtr hwnd;   // Window handle to the dialog box to display 
                    // information about the status of the file 
                    // operation. 
    public UInt32 wFunc;   // Value that indicates which operation to 
                    // perform.
    public IntPtr pFrom;   // Address of a buffer to specify one or more 
                    // source file names. These names must be
                    // fully qualified paths. Standard Microsoft®   
                    // MS-DOS® wild cards, such as "*", are 
                    // permitted in the file-name position. 
                    // Although this member is declared as a 
                    // null-terminated string, it is used as a 
                    // buffer to hold multiple file names. Each 
                    // file name must be terminated by a single 
                    // NULL character. An additional NULL 
                    // character must be appended to the end of 
                    // the final name to indicate the end of pFrom. 
    public IntPtr pTo;   // Address of a buffer to contain the name of 
                    // the destination file or directory. This 
                    // parameter must be set to NULL if it is not 
                    // used. Like pFrom, the pTo member is also a 
                    // double-null terminated string and is handled 
                    // in much the same way. 
    public UInt16 fFlags;   // Flags that control the file operation. 

    public Int32 fAnyOperationsAborted;  

                    // Value that receives TRUE if the user aborted 
                    // any file operations before they were 
                    // completed, or FALSE otherwise. 

    public IntPtr hNameMappings;                

                    // A handle to a name mapping object containing 
                    // the old and new names of the renamed files. 
                    // This member is used only if the 
                    // fFlags member includes the 
                    // FOF_WANTMAPPINGHANDLE flag.

    [MarshalAs(UnmanagedType.LPWStr)]
    public String lpszProgressTitle;           

                    // Address of a string to use as the title of 
                    // a progress dialog box. This member is used 
                    // only if fFlags includes the 
                    // FOF_SIMPLEPROGRESS flag.
}

So, what do we have here? First we have the hwnd, a handle to the owner window, as in many Shell API functions, the shell many times will present User Interface dialogs to get some input, so you need to specify which window will be the owner of these dialogs.

Next we have the wFunc value, this value sets which operation we are interested in, the options are Copy, Move & Delete, In fact there is one more option, Rename, but it is very limited and you can get the same effect with the Move operation, so I won't deal with it.

The pFrom parameter is the parameter where you set the source files, and the pTo parameter is where you set the destination files. Now you probably want to know how exactly you put a list of strings into a single IntPtr, Well this is NOT an array of strings. The guys who designed these APIs used a special technique to store a list of strings. They store it in one big string, with a NULL char between each string, and double NULL chars at the end of the string, so let's say we want to copy to files: "c:\file.txt" and "c:\file2.txt", the requested pFrom string should be: "c:\file.txt" + "\0" + "c:\file2.txt" + "\0\0" . Yep, this is indeed strange but that is the reason why the parameter pFrom cannot be marshaled as a normal string, instead I need to use Marshal.StringToHGlobalUni which gives me a pointer to a copy of the string on the native heap.

Next comes the fFlags parameter. This parameter lets us control some aspects of the file operation, we can set flags to do Silent Mode (not displaying the progress dialog box), we can set that 'Yes to All' will be the response for any dialog box that will be displayed, we can avoid presenting the user error dialogs when they occur and more.

The rest of the flags are less interesting. fAnyOperationAborted is where the ShFileOperation function will give the result of whether the operation was aborted. hNameMappings is rarely used and can help only if I'm interesting in the new names the user had to give during the operation. And finally lpszProgressTitle, if we set the flag FOF_SIMPLEPROGRESS, the progress dialog box does not present the file names and is supposed to present the text of this parameter. When I tested this function, I couldn't get this parameter to show, it didn't show the file names with the SIMPLEPROGRESS flag but it did not show the title parameter. What can I say, strange.

Now that we know how to do it, we will see the class I've made to wrap it up in a convenient way. The class is called ShellFileOperation. It includes two enums to make life easier called FileOperations and ShellFileOperationFlags. The class has the following properties:

// properties
public FileOperations Operation;
public IntPtr OwnerWindow;
public ShellFileOperationFlags OperationFlags;
public String ProgressTitle;
public String[] SourceFiles;
public String[] DestFiles;

I think no explanation is needed here. Also in the class is a small helper function that receives a string array and returns a string in the format I've mentioned earlier (double null terminated string), here is the code that does the job:

private String StringArrayToMultiString(String[] stringArray)
{
    String multiString = "";
 
    if (stringArray == null)
        return "";
 
    for (int i=0 ; i<stringArray.Length ; i++)
        multiString += stringArray[i] + '\0';
    
    multiString += '\0';
    
    return multiString;
}

And finally the most important function in the class DoOperation which creates a new struct, sets his fields, gets a pointer on the heap memory for our special From and To strings, and calls the function SHFileOperation with the struct. Here it is:

public bool DoOperation()
{
    ShellApi.SHFILEOPSTRUCT FileOpStruct = new ShellApi.SHFILEOPSTRUCT();
    
    FileOpStruct.hwnd = OwnerWindow;
    FileOpStruct.wFunc = (uint)Operation;
 
    String multiSource = StringArrayToMultiString(SourceFiles);
    String multiDest = StringArrayToMultiString(DestFiles);
    FileOpStruct.pFrom = Marshal.StringToHGlobalUni(multiSource);
    FileOpStruct.pTo = Marshal.StringToHGlobalUni(multiDest);
    
    FileOpStruct.fFlags = (ushort)OperationFlags;
    FileOpStruct.lpszProgressTitle = ProgressTitle;
    FileOpStruct.fAnyOperationsAborted = 0;
    FileOpStruct.hNameMappings = IntPtr.Zero;
 
    int RetVal;
    RetVal = ShellApi.SHFileOperation(ref FileOpStruct);
    
    ShellApi.SHChangeNotify(
        (uint)ShellChangeNotificationEvents.SHCNE_ALLEVENTS,
        (uint)ShellChangeNotificationFlags.SHCNF_DWORD,
        IntPtr.Zero,
        IntPtr.Zero);
 
    if (RetVal!=0)
        return false;
 
    if (FileOpStruct.fAnyOperationsAborted != 0)
        return false;
 
    return true;
}

Yes, I know, I didn't say anything about the SHChangeNotify. Although you are not obligated to use this function, it is recommended that after an application makes some changes to the file system, it will notify the changes to the shell, so it could update itself according to the changes. The SHChangeNotify is the way you do it, this is an another shell API function, that its job is to notify the shell, nothing more. It receives the event that happened (there is an enum), and two parameters, which depend on the event.

Here is an example of using this class, the following sample uses the shell copy to copy the files winmine.exe, freecell.exe and mshearts.exe from the system directory into the root directory. The first time the code will run, it shows a progress dialog box, the second time it also asks you if you want to override the previous files... just think about the code you would have to do to take care of all the possible failures while doing file IO operations.

ShellLib.ShellFileOperation fo = new ShellLib.ShellFileOperation();
 
String[] source = new String[3];    
String[] dest = new String[3];
 
source[0] = Environment.SystemDirectory + @"\winmine.exe";
source[1] = Environment.SystemDirectory + @"\freecell.exe";
source[2] = Environment.SystemDirectory + @"\mshearts.exe";
dest[0] = Environment.SystemDirectory.Substring(0,2) + @"\winmine.exe";
dest[1] = Environment.SystemDirectory.Substring(0,2) + @"\freecell.exe";
dest[2] = Environment.SystemDirectory.Substring(0,2) + @"\mshearts.exe";
    
fo.Operation = ShellLib.ShellFileOperation.FileOperations.FO_COPY;
fo.OwnerWindow = this.Handle;
fo.SourceFiles = source;
fo.DestFiles = dest;
 
bool RetVal = fo.DoOperation();
if (RetVal)
    MessageBox.Show("Copy Complete without errors!");
else
    MessageBox.Show("Copy Complete with errors!");

On to the next section.

Section 3: Adding Files to the Recent Documents List

The recent document list is a special folder that you can find using the SHGetFolderLocation or SHGetFolderPath API which were introduced in Part 1. But if you want to make changes to this directory, you shouldn't make it directly because the changes will not update properly and won't be reflected in the start menu. Instead, to make the changes in the appropriate way, you should use the API function SHAddToRecentDocs. So this is how this API looks like:

// Adds a document to the Shell's list of recently used documents or clears all
// documents from the list. 
[DllImport("shell32.dll")]
public static extern void SHAddToRecentDocs(
    UInt32 uFlags, // Flag that indicates the meaning of the pv parameter.
    IntPtr pv);    // A pointer to either a null-terminated string with the 

            // path and file name of the document, or a PIDL that 

            // identifies the document's file object. Set this parameter 

            // to NULL to clear all documents from the list. 
[DllImport("shell32.dll")]
public static extern void SHAddToRecentDocs(
    UInt32 uFlags,                    
    [MarshalAs(UnmanagedType.LPWStr)]
    String pv);

No, it's not a mistake, there are two declarations to this API. The first parameter can be one of two values: SHARD_PIDL or SHARD<CODE>_PATH (well actually, there is an ANSI and Unicode versions of the second value). If you activate the function with the PIDL flag, it means that the second parameter pv will hold the PIDL of the file you want to add, But, if you put the PATH flag in the flags parameter, then it means that the second parameter is a string, So because the second parameter can be sometimes a IntPtr (when you use the PIDL flag) and sometimes a string (when you use the PATH flag), I've written two declarations to this API.

Also, I've written a small wrapper class for this function. It includes the enum for the possible flags and two static methods, one for adding a new item to the document list and one to clear the list. Here is the code for the class:

public class ShellAddRecent
{
    public enum ShellAddRecentDocs
    {
        SHARD_PIDL  = 0x00000001,    // The pv parameter points to a 

                    // null-terminated string with the path 
                    // and file name of the object.
        SHARD_PATHA = 0x00000002,    // The pv parameter points to a pointer 
                    // to an item identifier list (PIDL) 
                    // that identifies the document's file 
                    // object. PIDLs that identify nonfile 
                    // objects are not allowed.
        SHARD_PATHW = 0x00000003      // same as SHARD_PATHA but unicode 
                    // string
    }
 
public static void AddToList(String path)
{
        ShellApi.SHAddToRecentDocs((uint)ShellAddRecentDocs.SHARD_PATHW,path);    
    }
 
    public static void ClearList()
    {
        ShellApi.SHAddToRecentDocs((uint)ShellAddRecentDocs.SHARD_PIDL,
                                   IntPtr.Zero);
    }
}

Nothing to explain here. One thing to note is that if you want to clear the list, you just need to put a null in the second parameter of the SHAddToRecentDocs function. Here is how you use the class:

ShellLib.ShellAddRecent.AddToList(@"c:\windows\Rhododendron.bmp");
ShellLib.ShellAddRecent.ClearList();

Section 4: Managing Printers

Remember section 1? with the ShellExecute stuff? and verbs? So if you want to print something that is printable, like a Word document or a bitmap, you just need to use the shell execute command with the verb "print". That's pretty easy, I know. But there is some more printer stuff you can do with an API named SHInvokePrinterCommand. Here is its C# declaration:

// Executes a command on a printer object. 
[DllImport("shell32.dll")]
public static extern Int32 SHInvokePrinterCommand(
 
    IntPtr hwnd, // Handle of the window that will be used as the parent 
            // of any windows or dialog boxes that are created 
            // during the operation.

    UInt32 uAction, // A value that determines the type of printer 
            // operation that will be performed.
 
    [MarshalAs(UnmanagedType.LPWStr)]
    String lpBuf1, // Address of a null-terminated string that contains 

            // additional information for the printer command. 
    [MarshalAs(UnmanagedType.LPWStr)]    
    String lpBuf2,            // Address of a null-terminated string that contains 

            // additional information for the printer command. 
    Int32 fModal);          //  value that determines whether 

            // SHInvokePrinterCommand should return after 
            // initialising the command or wait until the command 
            // is completed.

The first parameter is where you put the owner window handle, the second parameter, uAction is the type of command you want to perform, you can take one of the PrinterActions enum values. lpBuf1 and lpBuf2 are two parameters which depend on the action being performed. The last parameter, fModal sets whether the function should wait with its return to the end of the command or should it return immediately. Here are three samples of using this function:

Opening a printer:

int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
    this.Handle,
    (uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_OPEN,
    "printer name comes here",
    "",
    1);

Showing the printer properties:

int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
    this.Handle,
    (uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_PROPERTIES,
    "printer name comes here",
    "",
    1);

Printing a test page:

int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
    this.Handle,
    (uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_TESTPAGE,
    "printer name comes here",
    "",
    1);

As you see, very simple. No wrapper class needed.

Extra Notes

Well, I thought about doing a section about Drag and Drop capabilities using the shell but I've discovered that C# supports all there is to it, so using the Shell API will be a total waste of time.

This finalizes the part of USING the Shell. The next article will be about extending the shell which lets you do very interesting things. Note that I'm skipping some MSDN article who talks about "extending" the shell using only registry manipulations. If you want to know how to do that, you should visit the following MSDN link (there is no programming involved), Shell Basics: Extending the Shell.

Update (09.01.2010)

I turns out I had a bug in the definition of SHFILEOPSTRUCT structure which prevented some of the mentioned features to work properly.

The code was updated as follows: ShellNameMapping.cs:

  • New class / file added
ShellFileOperation.cs:
  • Added NameMappings property and handling code to populate this property after the copy / move operation.
ShellApi.cs:
  • Added SHNAMEMAPPINGSTRUCT, SHNAMEMAPPINGINDEXSTRUCT and SHFreeNameMappings() to receive the NameMappings
  • Added SHFILEOPSTRUCT32, SHFILEOPSTRUCT64, SHFileOperation32(), SHFileOperation64() declarations
  • Changed SHFileOperation() to be a proxy that is calling the 32 or 64 version depending on the target machine
  • Added GetMachineType()

The first three changes are described here.

The last three are crucial to run on 32 and 64 bit machines without crashes.

The new code provides an additional NameMappings property on the ShellFileOperation class that is populated after the copy / move operation. With this property, one can get the new names the user had to give during the file operation (e.g. if a target file already exists on Vista or Windows 7, he can copy it with a changed name). Also the code is now working on 32 and 64 bit machines.

Credit goes to Wolfram Bernhardt and Benjamin Schröter for finding and fixing the bug.

That's it.

Hope you like it. Don't forget to vote.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

About the Author

Arik Poznanski
Software Developer (Senior) Verint
Israel Israel
Arik Poznanski is a senior software developer at Verint. He completed two B.Sc. degrees in Mathematics & Computer Science, summa cum laude, from the Technion in Israel.
 
Arik has extensive knowledge and experience in many Microsoft technologies, including .NET with C#, WPF, Silverlight, WinForms, Interop, COM/ATL programming, C++ Win32 programming and reverse engineering (assembly, IL).
Follow on   Twitter   Google+

Comments and Discussions

 
GeneralMy vote of 5 Pinmembermanoj kumar choubey13-Feb-12 20:03 
QuestionShellFileOperation using String[] [modified] PinmemberMalcolm J Stewart17-May-11 3:36 
AnswerRe: ShellFileOperation using String[] PinmvpArik Poznanski18-May-11 0:37 
AnswerRe: ShellFileOperation using String[] PinmemberMalcolm J Stewart20-Jun-11 3:22 
GeneralMy vote of 5 PinmemberECP124-Feb-11 17:23 
GeneralMy vote of 5 PinmemberKyle Wood19-Oct-10 3:14 
GeneralMy vote of 5 Pinmemberthileep201027-Jun-10 8:35 
General[bug] File copy operation - Always display confirmation dialog. Pinmemberjahfer12-Apr-10 15:49 
GeneralNice work! Pinmemberxzz01953-Dec-09 7:41 
GeneralCool! PinmemberRayShaw3-Aug-09 17:13 
QuestionC# Recycle bin browsing PinmemberMember 415744919-Apr-09 8:03 
GeneralPrinting Command is not working - Printer not found Pinmemberjakobdoppler23-Nov-08 22:38 
GeneralRe: Printing Command is not working - Printer not found PinmemberMember 281782714-Jan-09 23:16 
The Printer must be installed as local and named like "Color Jet XXXX on Hermes"
Good luck!
GeneralCannot Execute SORT.EXE PinmemberMember 45577024-Sep-08 22:27 
GeneralView document section Pinmemberpcjd633-Sep-08 2:37 
QuestionfAnyOperationsAborted always 0? PinmemberGarland219-Feb-08 13:21 
QuestionCan you help me to create virtual drive using shell extension in C# Pinmembersherwood12345630-Oct-07 22:41 
AnswerRe: Can you help me to create virtual drive using shell extension in C# Pinmemberalhambra-eidos1-Aug-08 9:38 
GeneralMaking hNameMappings work PinmemberTih200718-Sep-07 4:48 
GeneralThanks! PinmemberPaolo Benjamin T. Briones7-Jul-07 3:12 
GeneralShellextension when Pinmemberpalmenlars29-Jun-07 1:53 
QuestionHow to get time that require to copy files. PinmemberRakesh B Singh23-Jun-07 1:26 
QuestionVista? PinmemberRobin Debnath13-May-07 19:35 
AnswerRe: Vista? Pinmembercschmidt31-May-07 4:31 
GeneralGood article Pinmemberblackstorm26-Apr-07 20:04 

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
Web04 | 2.8.140415.2 | Last Updated 12 Jan 2010
Article Copyright 2003 by Arik Poznanski
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid