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

Simple FTP library in C#

, 3 May 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
A complete walkthrough for FTP protocol in C#

Introduction

This is my maiden article in CodeProject. Here I shall be illustrating the usage of FTP in C# with an easy to use library that I have coded. I have used the classes FtpWebRequest and FtpWebResponse from the namespace System.Net,  and they are sealed classes (cannot be inherited). 

I have currently implemented these functions of FTP:

  • listing files, listing directories  
  • uploading, downloading files  
  • creating/deleting directories and deleting files 

Internals of the T-FTP class

The FtpModeUsePassive,CurrentDirectory, FtpServer, FtpPassword,FtpUserName are the properties which represent the server name, FTP user, and FTP password,and mode name(boolean type) namely passive and acticve. 

 public bool FtpModeUsePassive
        {
            get;
            set;

        }

        public string CurrentDirectory
        {
            get;
            set;


        }
        public string FtpServer
        {
            get;
            set;

        }
        public string FtpUserName
        {
            get;
            set;

        }
        public string FtpPassword
        {
            get;
            set;

        } 

Remove files/directories and make new directories

We create the instance of FtpWebRequest class with the FTP URL for the particular file or directory. Eg., if the file is in ftp://2shar.servehttp.com/httpdocs/myfiles/ then the corresponding URL would be as follows in the code:

FtpWebRequest request = (FtpWebRequest)WebRequest.Create(new Uri("ftp://" + FtpServer + serverUri));

Next, reqFTP.Credentials property takes object of class NetworkCredential created with the constructor specifying the username and password as arguments (we can also specify domain name in the constructor of NetworkCredential).

request.Credentials = new NetworkCredential(FtpUserName, FtpPassword);  

The KeepAlive property specifies whether the connection has to be left open after the corresponding process by default the value is true.

UseBinary property specifies the mode of FTP operation you can opt for either binary or ascii mode. Each has its scenario in cases where you concerned with mostly text files and documents with ascii format texts you can initialise the property to false while if you have to transfer files such as media or pictures or any zip files where binary streams of data are involved its better to opt for binary that is set property to true.

In case of file upload there is a ContentLength property that has to be set with the length of the file that you are uploading.

FileInfo class provides you with the way to do that FileInfo class has property Length (gives us the size of the file), Name (gives the name of the file), FullName (full path for the file).

Method property of FtpWebRequest gives us the purpose of the request( whether you want to create a directory delete a directory or a file).

 request.Method = WebRequestMethods.Ftp.DeleteFile; 
request.Method =  WebRequestMethods.Ftp.MakeDirectory; 
request.Method = WebRequestMethods.Ftp.RemoveDirectory;

Now we get the response for the FtpWebRequest and place it in the FtpWebRespnse object the response.StatusDescription gives us the information about the status of the FTP request which can be passed to the caller any exceptions can be handled by using a try catch block and can return the exception to the caller.

 public override string FtpMakeDirectory(string serverUri)
        {

            try
            {

                FtpWebRequest request = (FtpWebRequest)WebRequest.Create(new Uri("ftp://" + FtpServer + serverUri));
                request.Credentials = new NetworkCredential(FtpUserName, FtpPassword);
                request.UsePassive = FtpModeUsePassive;
                request.KeepAlive = false;
                request.UseBinary = true;
                request.Method = WebRequestMethods.Ftp.MakeDirectory;

                FtpWebResponse response = (FtpWebResponse)request.GetResponse();

                response.Close();
                return response.StatusDescription.ToString();
            }
            catch (Exception SomeException)
            {
                return "error-" + SomeException.Message.ToString();
            }
        }







        // performs the FTP remove directory

        public override string FtpRemoveDirectory(string serverUri)
        {

            try
            {

                FtpWebRequest request = (FtpWebRequest)WebRequest.Create(new Uri("ftp://" + FtpServer + serverUri));
                request.Credentials = new NetworkCredential(FtpUserName, FtpPassword);
                request.UsePassive = FtpModeUsePassive;
                request.KeepAlive = false;
                request.UseBinary = true;
                request.Method = WebRequestMethods.Ftp.RemoveDirectory;

                FtpWebResponse response = (FtpWebResponse)request.GetResponse();

                response.Close();
                return response.StatusDescription.ToString();
            }
            catch (Exception SomeException)
            {
                return "error-" + SomeException.Message.ToString();
            }
        } 

File Upload and Download

When an user is downloading a file one has to create the instance of FtpWebRequest class with the FTP URL for the particular file (e.g., if the file is in ftp://2shar.servehttp.com/httpdocs/myfile.zip then the corresponding URL would be as follows in the code. 

 request = (FtpWebRequest)FtpWebRequest.Create(new Uri("ftp://" + FtpServer + url + fileInf.Name));

Next, request.Credentials property takes object of class NetworkCredential created with the constructor specifying the username and password as arguements (we can also specify domain name in the constructor of NetworkCredential).

request.Credentials = new NetworkCredential(FtpUserName, FtpPassword); 

The KeepAlive property specifies whether the connection has to be left open after the corresponding process by default the value is true.

UseBinary property specifies the mode of FTP operation you can opt for either binary or ASCII mode. Each has its scenario in cases where you concerned with mostly text files and documents with ascii format texts you can initialise the property to false while if you have to transfer files such as media or pictures or any zip files where binary streams of data are involved its better to opt for binary that is set property to true.

In case of file upload there is a ContentLength property that has to be set with the length of the file that you are uploading.

The FileInfo class provides you with the way to do that FileInfo class has property Length (gives us the size of the file), Name (gives the name of the file), FullName (full path for the file).

Method property of FtpWebRequest gives us the purpose of the request (whether you want to upload, download or delete etc..)

 request.Method = WebRequestMethods.Ftp.DownloadFile;

When we are downloading we initialize a new instance of the FileStream class with the specified path, creation mode, and write permission.

FileStream fileStreamObject = new FileStream(downloadPath + fileInf.Name, FileMode.Create, FileAccess.Write);

We can get the size of the download file using the FtpWebRequest.Method by setting the property to  WebRequestMethods.Ftp.GetFileSize  FtpFileSize() does the job in this case. The response to the request created would give the size of file.

By taking a size/100 as the size of the byte buffer which we can use to read from the response stream and will be helpful to show the status and time of download.

We check the downloadFile event to be not equal to null so that there wouldn't be any unwanted behavior at run time due to event not being initialised by the user. 

long tempLength = FtpFileSize("ftp://" + FtpServer + fileInf.ToString());
                byte[] bufferByte = new byte[tempLength / 100];
                int contentLength;
                contentLength = responseStream.Read(bufferByte, 0, bufferByte.Length);

                long tempLength1 = tempLength;
                while (contentLength != 0)
                {

                    fileStreamObject.Write(bufferByte, 0, contentLength);
                    contentLength = responseStream.Read(bufferByte, 0, bufferByte.Length);
                    tempLength -= contentLength;
                    if (tempLength > 0)
                    {
                        if (downloadFile != null)
                        {
                            downloadFile(tempLength1, tempLength);

                        }
                    }
                } 

Then we can read from the response stream and write the bytes that are read into the filestream object.

contentLength = responseStream.Read(bufferByte, 0, bufferByte.Length);

fileStreamObject.Write(bufferByte, 0, contentLength); 

Once we have completely read from the response stream we will close the filestream object and return the status of the response object to inform the caller about the status of his request (i.e., whether the file has been downloaded or not).

 fileStreamObject.Close();
                responseStream.Close();
                return response.StatusDescription.ToString();

public override event DisplayDownloadDelegate downloadFile;
        public override event DisplayUploadDelegate uploadFile;
        public override event CollectFilesDelegate displayCollectFiles;
        public override event CollectDirectoryDelegate displayCollectDirectory;

//we have created the events of corresponding delegate type so that user can just subcribe
// to the event and display the result in the way they intend to. 
//we have overridden the event because we have used a top abstract class and are using the object
//the abstract class and instantiating the object using subclasses.

The events and delegates for the upload and download are given above, this approach is used to implement the subscriber-publisher design where the tftp.dll has published set of events namely uploadFile  and downloadFile which corresponds to type of delegates DisplayUploadDelegate and DisplayDownloadDelegate.

Now a person using the DLL file can subscribe to the event by using a function of his own function which should also has to be of the delegate type DisplayUploadDelegate and DisplayDownloadDelegate.

These functions of the user will be called whenever the event is raised. If you look at the code of download you can notice that we are raising the event in FtpFileDownload function downloadFile(len1,len) after reading each set of bytes from the response stream doing so we are calling the subscriber function that the person using the dll has written.

The functions that handle the download (FtpFileDownload) and upload (FtpFileUpload) are listed below.

//FTP file Upload

        public override string FtpFileUpload(string fileName, string url)
        {

            try
            {
                FileInfo fileInf = new FileInfo(fileName);
                string uri = "ftp://" + FtpServer + "/" + url + fileInf.Name;
                FtpWebRequest request;

                request = (FtpWebRequest)FtpWebRequest.Create(new Uri("ftp://" + FtpServer + url + fileInf.Name));

                request.Credentials = new NetworkCredential(FtpUserName, FtpPassword);
                request.UsePassive = FtpModeUsePassive;
                request.KeepAlive = false;
               
                request.Method = WebRequestMethods.Ftp.UploadFile;

                request.UseBinary = true;

                request.ContentLength = fileInf.Length;

                long buffLength = fileInf.Length / 100;
                byte[] bufferByte = new byte[buffLength];
                int contentLength;

                FileStream fileStreamObject = fileInf.OpenRead();
                FtpWebResponse resp = (FtpWebResponse)request.GetResponse();
                Stream streamObject = request.GetRequestStream();

                contentLength = fileStreamObject.Read(bufferByte, 0, (int)buffLength);

                long tempLength = fileStreamObject.Length;
                while (contentLength != 0)
                {
                    streamObject.Write(bufferByte, 0, contentLength);
                    contentLength = fileStreamObject.Read(bufferByte, 0, (int)buffLength);
                    tempLength -= contentLength;
                    if (tempLength > 0)
                    {
                        if (uploadFile != null)
                        {
                            uploadFile(fileStreamObject.Length, tempLength);

                        }
                    }
                }

                streamObject.Close();
                fileStreamObject.Close();

                return resp.StatusDescription.ToString();

            }
            catch (Exception SomeException)
            {

                return SomeException.Message.ToString();
            }
        }

        //Get size of the file


        public override long FtpFileSize(string url)
        {

            FtpWebRequest reqSize = (FtpWebRequest)FtpWebRequest.Create(new Uri(url));
            reqSize.Credentials = new NetworkCredential(FtpUserName, FtpPassword);
            reqSize.Method = WebRequestMethods.Ftp.GetFileSize;
            reqSize.UseBinary = true;
            reqSize.UsePassive = FtpModeUsePassive;
            
            FtpWebResponse respSize = (FtpWebResponse)reqSize.GetResponse();
            respSize = (FtpWebResponse)reqSize.GetResponse();
            long size = respSize.ContentLength;
            respSize.Close();
            return size;

        }


        //Ftp file download

        public override string FtpFileDownload(string fileUrl, string downloadPath)
        {

            try
            {
                FileInfo fileInf = new FileInfo(fileUrl);
                string uri = "ftp://" + FtpServer + "/httpdocs/" + fileInf.Name;
                FtpWebRequest  request;

                 request = (FtpWebRequest)FtpWebRequest.Create(new Uri("ftp://" + FtpServer + fileInf.ToString()));

                 request.Credentials = new NetworkCredential(FtpUserName, FtpPassword);
                 request.UsePassive = FtpModeUsePassive;
                 request.UseBinary = true;
                 request.KeepAlive = false;

                 request.Method = WebRequestMethods.Ftp.DownloadFile;


                FtpWebResponse response = (FtpWebResponse) request.GetResponse();

                Stream responseStream = response.GetResponseStream();


                FileStream fileStreamObject = new FileStream(downloadPath + fileInf.Name, FileMode.Create, FileAccess.Write);

                long tempLength = FtpFileSize("ftp://" + FtpServer + fileInf.ToString());
                byte[] bufferByte = new byte[tempLength / 100];
                int contentLength;
                contentLength = responseStream.Read(bufferByte, 0, bufferByte.Length);

                long tempLength1 = tempLength;
                while (contentLength != 0)
                {

                    fileStreamObject.Write(bufferByte, 0, contentLength);
                    contentLength = responseStream.Read(bufferByte, 0, bufferByte.Length);
                    tempLength -= contentLength;
                    if (tempLength > 0)
                    {
                        if (downloadFile != null)
                        {
                            downloadFile(tempLength1, tempLength);

                        }
                    }
                }


                fileStreamObject.Close();
                responseStream.Close();
                return response.StatusDescription.ToString();



            }
            catch (Exception SomeException)
            {
                return SomeException.Message.ToString();

            }
        }

File and directory List

I will explain the directory retrieval now. First, we have to create the instance of the FtpWebRequest class with the FTP URL for the particular fdirectory (e.g., if the directory is ftp://2shar.servehttp.com/httpdocs/, then the corresponding URL for the request would be as follows. 

request = (FtpWebRequest)FtpWebRequest.Create(new Uri("ftp://" + FtpServer + url)); 

Next, reqFTP.Credentials property takes object of class NetworkCredential created with the constructor specifying the username and password as arguments (we can also specify domain name in the constructor of NetworkCredential).

request.Credentials = new NetworkCredential(FtpUserName, FtpPassword); 

KeepAlive property specifies whether the connection has to be left open after the corresponding process by default the value is true.

UseBinary property specifies the mode of FTP operation you can opt for either binary or ASCII mode. Each has its scenario in cases where you concerned with mostly text files and documents with ascii format texts you can initialise the property to false while if you have to transfer files such as media or pictures or any zip files where binary streams of data are involved its better to opt for binary that is set property to true.

FileInfo class provides you with the way to do that FileInfo class has property Length (gives us the size of the file), Name (gives the name of the file), FullName (full path for the file).

Method property of FtpWebRequest gives us the purpose of the request( whether you want to upload, download or delete or list files or directory etc..)

We use  WebRequestMethods.Ftp.ListDirectoryDetails so that we can display the details of creation date and size associated with each files, if only the file name is to be displayed then we can use WebRequestMethods.Ftp.ListDirectory.

request.Method = WebRequestMethods.Ftp.ListDirectoryDetails; 

Thereafter we get the response for the FTP request and store it in a FtpWebResponse object (response) and get the stream for the response object and read the stream with help of a StreamReader object.The code is given below.

 FtpWebResponse response = (FtpWebResponse)request.GetResponse();

                Stream responseStream = response.GetResponseStream();
                StreamReader readerObject = new StreamReader(responseStream); 

We can carefully index out the directory name and directory creation date from the string read by the StreamReader class use methods of String class namely String.IndexOf(string value,int startindex) use String.subString(int startindex,int length).

 if (processString.IndexOf("<DIR>", 0) != -1)
                    {
                        string processedString = RemoveWhiteSpaces(processString);
                        dirdate = processedString.Substring(0, processedString.IndexOf("<DIR>", 0));
                        dirname = processedString.Substring(processedString.IndexOf("<DIR>", 0) + 5, processedString.Length - (processedString.IndexOf("<DIR>", 0) + 5));
                     listDirectoryCollection.Add(dirname, dirdate);
                        displayCollectDirectory(dirname, dirdate);

                    } 

Now we can get the status of operation by returning the status of response object.

return response.StatusDescription.ToString(); 

The functions that handle the list directories (FtpListDirectories) and list files (FtpListFiles) are listed below.

   // resolving and formatting display of files 
        public override string[] ResolveFiles(string filesString)
        {
            string resultString = string.Empty;
            char[] arrayOfCharacters = filesString.ToCharArray();
            bool flags = false;
            foreach (char singleCharacter in arrayOfCharacters)
            {
                if (singleCharacter == ' ')
                {
                    if (flags == false)
                    {
                        resultString = resultString + "#";
                    }
                    flags = true;


                }
                else
                {
                    resultString = resultString + singleCharacter;
                    flags = false;
                }
            }

            char[] seperatorCharacter = { '#' };
            string[] returnStringArray = resultString.Split(seperatorCharacter);
            return returnStringArray;

        }




        //remove white spaces

        public override string RemoveWhiteSpaces(string processString)
        {
            string resultString = string.Empty;
            char[] arrayOfCharacters = processString.ToCharArray();
            foreach (char singleCharacter in arrayOfCharacters)
            {
                if (singleCharacter != ' ')
                {
                    resultString = resultString + singleCharacter.ToString();
                }
            }
            return resultString;
        }


        //perform listing of directories on server


        public override string FtpListDirectories(string url)
        {
            listDirectoryCollection = new Dictionary<string, string>(100000);
            try
            {
                string uri = FtpServer + url;
                FtpWebRequest request;

                request = (FtpWebRequest)FtpWebRequest.Create(new Uri("ftp://" + FtpServer + url));

                request.Credentials = new NetworkCredential(FtpUserName, FtpPassword);
                request.UsePassive = FtpModeUsePassive;
                request.KeepAlive = true;

                request.Method = WebRequestMethods.Ftp.ListDirectoryDetails;

                request.UseBinary = true;
                FtpWebResponse response = (FtpWebResponse)request.GetResponse();

                Stream responseStream = response.GetResponseStream();
                StreamReader reader = new StreamReader(responseStream);
                
                string processString;
                string dirname, dirdate;
                processString = reader.ReadLine();
                while (processString != null)
                {
                    if (processString.IndexOf("<DIR>", 0) != -1)
                    {
                        string processedString = RemoveWhiteSpaces(processString);
                        dirdate = processedString.Substring(0, processedString.IndexOf("<DIR>", 0));
                        dirname = processedString.Substring(processedString.IndexOf("<DIR>", 0) + 5, processedString.Length - (processedString.IndexOf("<DIR>", 0) + 5));
                     listDirectoryCollection.Add(dirname, dirdate);
                        displayCollectDirectory(dirname, dirdate);

                    }
                    else
                    {
                       

                    }
                    processString = reader.ReadLine();

                }

                reader.Close();
                response.Close();
                return "ok";
            }
            catch (Exception SomeException)
            {

                return SomeException.Message.ToString();
            }
        }


        // perform file listing on server


        public override string FtpListFiles(string url)
        {
            listFileCollection = new Dictionary<string, string>(100000);
            try
            {
                string uri = "ftp://" + FtpServer + url;
                FtpWebRequest request;

                request = (FtpWebRequest)FtpWebRequest.Create(new Uri("ftp://" + FtpServer + url));

                request.Credentials = new NetworkCredential(FtpUserName, FtpPassword);

                request.KeepAlive = true;
                request.UsePassive = FtpModeUsePassive;
                request.Method = WebRequestMethods.Ftp.ListDirectoryDetails;
                
                request.UseBinary = true;

                FtpWebResponse response = (FtpWebResponse)request.GetResponse();

                Stream responseStream = response.GetResponseStream();
                StreamReader readerObject = new StreamReader(responseStream);
                string processString;
                string dirname, dirdate;
                processString = readerObject.ReadLine();
                while (processString != null)
                {
                    if (processString.IndexOf("<DIR>", 0) != -1)
                    {
                        string processedString = RemoveWhiteSpaces(processString);
                        dirdate = processedString.Substring(0, processedString.IndexOf("<DIR>", 0));
                        dirname = processedString.Substring(processedString.IndexOf("<DIR>", 0) + 5, processedString.Length - (processedString.IndexOf("<DIR>", 0) + 5));

                    }
                    else
                    {
                        string[] processedString = ResolveFiles(processString);
                        listFileCollection.Add(processedString[3].ToString(), (Int32.Parse(processedString[2].ToString()) / 1024).ToString());
                        displayCollectFiles(processedString[3].ToString(), (Int32.Parse(processedString[2].ToString()) / 1024).ToString(), processedString[0] + "-" + processedString[1]);

                    }
                    processString = readerObject.ReadLine();

                }

                readerObject.Close();
                response.Close();
                return response.StatusDescription.ToString();
            }
            catch (Exception SomeException)
            {

                return SomeException.Message.ToString();
            }
        }

Using T-FTP Library in your project  

In the introduction tftp library interiors and working has been demonstrated now I shall explain on how you can hook this up with your project.

Properties

property name semantics datatype 
FtpUserNamerepresents the ftp username  string 
FtpServer represents ftp address string 

FtpPassword 

represents ftp user password string  

FtpModeUsePassive

select betwen two modes bool

Example

 TusharFtp.FtpClass ftpObject = new TusharFtp.FtpHelperClass();
                ftpObject.FtpServer = servers;
                ftpObject.FtpUserName = users;
                ftpObject.FtpPassword = pass;
                ftpObject.FtpModeUsePassive = true; 

1. Upload (with the status of upload)

If uploadme is the name of your function that is subscribing to the event upload_file.

Variables a and b represents total size in bytes and size yet to be downloaded/uploaded for the file.

//
static void UploadMe(long totalLength, long currentLength)
{
    Console.WriteLine(currentLength + " out of " + totalLength +"has been left to downloaded so far");
}

ftpObject.uploadFile += new tftp.tftp.DisplayUploadDelegate(UploadMe);
//hooking up the event 'uploadFile' with your 'UploadMe' function of delegate type 'DisplayUploadDelegate'
Console.WriteLine( ftpObject.FtpFileUpload("e:\\splits.zip","/httpdocs/"));
//calling the FtpFileUpload function with parameters path of the file,the url on server where it has to be uploaded
//

Similarly for the download function.

2. Listing directories

If FtpListDirectories is the name of your function that is subscribing to the event displayCollectDirectory.

FtpListDirectories is the function in the DLL that gives you the list of the directories ,the parameters of the function represent name of the directory and creation date of the directory.

//
static void ListDirectories(string directoryName,string directoryCreateDate)
{
    Console.WriteLine("directory name= "+directoryName+" directory creation date= "+directoryCreateDate);
}

ftpObject.displayCollectDirectory += new TusharFtp.CollectDirectoryDelegate(ListDirectories);
//hooking up the event 'displayCollectDirectory' with your
//'ListDirectories' function of type delegate  'CollectDirectoryDelegate'
  
ftpObject.FtpListDirectories("/httpdocs/");
//listing the directories in url ftp://website.com/httpdocs
//

Similarly for listing files, we can use the FtpListFiles() function.

3. Listing files

If FtpListFiles is the name of your function that is subscribing to the event displayCollectFiles.

FtpListFiles is the function in the DLL that gives you the list of the files, the parameters of the function represent name of the file, the size of the file and creation date of the file.

//
static void ListFiles(string fileName,string fileSize,string fileCreateDate)
{
    Console.WriteLine("file name= "+fileName+" file size= "+fileSize+" file creation date "+fileCreateDate);
}

ftpObject.displayCollectFiles += new TusharFtp.CollectFilesDelegate(listdir);
//hooking up the event 'displayCollectFiles' with
//your 'ListFiles' function of type delegate  'CollectFilesDelegate'

ftpObject.FtpListFiles("/httpdocs/");
//listing the files in url ftp://website.com/httpdocs

4. Make directory and delete directory and delete files

These functions have string return types which informs about the status of the operation. FtpMakeDirectory, FtpRemoveDirectory,FtpDeleteFile represents the three functions for making directory, deleting directory, and deleting file.

//
string makeDirectoryStatus = ftpObject.FtpMakeDirectory("httpdocs/tftp");
string removeStatus = ftpObject.FtpRemoveDirectory("httpdocs/tftp");
string deleteStatus=ftpObject.FtpDeleteFile("httpdocs/splits.zip"); 
// 

Points of interest   

Thanks for reading through, you can learn more about the FTPWebRequest class in MSDN and FTPWebResponse class in MSDN.

License

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

Share

About the Author

tushar-2shar
Software Developer slswolution
India India
I enjoy coding , i have 1 year experience in .net and php, i am comparatively new hope to learn a alot as the time proceeds.

Comments and Discussions

 
QuestionBroken link PinmemberWrangly26-May-14 5:48 
QuestionPlease give me download link - Download t-ftp-project.zip - 48.5 KB - :confused::confused: Pinmemberlinhttt26-Sep-13 5:49 
GeneralProject Link Error Pinmemberbarisyigit9-Mar-13 19:33 
QuestionNice Article PinmemberLindsay Fisher4-Mar-13 4:17 
QuestionAbout API Pinmemberduonghv12-Sep-12 19:18 
BugDownload t-ftp-project.zip - 48.5 KB : download not available! Pinmemberdnash621-Jun-12 12:19 
GeneralMy vote of 4 PinmemberShahin Khorshidnia4-May-12 1:25 
GeneralMy vote of 2 [modified] PinmemberShahin Khorshidnia26-Apr-12 12:16 
GeneralRe: My vote of 2 Pinmembertushar-2shar3-May-12 22:46 
GeneralGood Work PinmemberRohit Vipin Mathews13-Mar-12 23:09 

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 | Terms of Use | Mobile
Web04 | 2.8.1411028.1 | Last Updated 4 May 2012
Article Copyright 2012 by tushar-2shar
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid