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

Answering machine(TAPI 2.1)

, 10 Sep 2006 CPOL
Rate this:
Please Sign up or sign in to vote.
Some description about TAPI + a sample incomplete answering machine

Sample Image - AppScShot_1.jpg

Introduction

This article tries to describe and simplify developing an answering machine, using Microsoft Telephony Application Programming Interface(TAPI) and MFC. It just describes TAPI 2.x. and has nothing to do with COM!

The sample is not a complete answering machine (since it does not record any voice). In fact it is more similar to an IVR. It answers the phone, catches caller ID and responds to digits pressed by the user.

Beside, this article tries to overcome some of the common problems and difficulties like CallerID, DTMF and Voice! which relative question can be seen every where in TAPI forums.

Finally I'll introduce a simple architecture and some useful resources on the web.

Acknowledgement and disclaimers

Since number of links were a lot, I added them where ever needed inside the article, and also at the bottom of the article. Great thanks to all their authors and thanks to their kindness.

Background

TAPI

TAPI stands for Telephony Application Programming Interface and is a set of API's designed by Intel and Microsoft jointly to make it easy to write application for telephony communication devices (like modems). With their help we no longer need to know a lot of hardware and their relative protocol details (like AT commands for modems).

Using these set of API's you'll be able to write applications capable of sending data, voice, fax and also receive them. It is now possible to write a wide range of systems like VOIP, PBX, CallControl, IVR, Video Conferences, ... . With this wide range of applications, there is no need to say these set of API's are difficult to use! (Before seeing them I thought development in windows is really very easy!)

Versions

These API set have a lot of versions ( At the time writing this article and for C++: 1.3, 1.4, 2.0, 2.1, 2.2, 3.0, 3.1) But there has been a major change in moving from ver. 2.2 to 3.0 : before ver 3.x they were C based API's working with a group of structures for data input or output. With ver. 3.x (Completed in Windows XP) Microsoft made a significant change, in order to make TAPI development available for all programming languages (Not just C++), and it was using COM (Component Object Model).

So, TAPI 3.x is COM based and possibly MFC is not best platform for writing such applications(COM Based). I'm not familiar with COM. I just read Michel Dun's nice article about it. Perhaps if I learned it well, I write an article using TAPI 3.x, but this article talks about TAPI 2.x and specially TAPI 2.1.

One of the initializing jobs, using TAPI, is to negotiate with the TSP for a supported version. Look at lineNegotiateAPIVersion in MSDN.

Now that I mentioned Michael Dunn, I should say there is a TAPI site with his name :http://www.rainyjay.com/tapi/tapi.htm Who knows, perhaps this site belongs to the famous Michael Dunn in Code Project!

TSP 

There is a mechanism inside TAPI that makes it possible for different hardware manufacturer to give the developer a single interface to work with. While our application works with the set of APIs there is another side which is modem driver. manufacturer of the modem should write a driver capable of doing main TAPI functions. Look at below figure:

TAPI - Big picture

In windows, there's a service called TAPI, you can find it in the services list in administrative tools of the control panel, which will run by the first call to a TAPI, if it has not been running already, and does appropriate calls if the TSP is available. TSP's are written by the manufacturer of your hardware and respond to TAPI calls, so for an example when you seek a device which is capable of playing voice on a line, Windows checks if the required TSP exists, if so you can do later calls. As figure shows in TAPI 2.x all calls will be done using some API's defined in a library file: 'TAPI32.dll', the dll function then using the service calls the TSP, after this call, TSP is responsible to do the job.
So, sometimes we should not blame TAPI for this or that, it is up to the TSP and the driver to comply with TAPI.

Line and Phone, Mic/SPK jacks

TAPI - Big picture

I know! Something clear, but to me a man who had nothing to do with his modem but connect to the Internet or transfer files using Hyper Terminal, these terms were not meaningful at first. this is a tutorial and that's why I'll describe them here.

Look at your modem carefully, you'll see two big sockets(Like RJ45) at the bottom of it, one of them named Line and the other named Phone. Many people use them every day. You might have been connecting your phone to it's phone socket and line to your local line system which will connect you to a PSTN. Remember these terms. you'll hear them a lot working with TAPI.

There is also, in some modems, two jacks marked as Mic and Speaker. you can use them to connect a line to a Headphone, if modem supports, for example. In my sample App, you'll see all API's I used have 2 versions some start with line (like lineOpen) and a series start with phone (like phoneOpen).

lineOpen tries to connect to the line using modem, while phoneOpen tries to open a phone device you selected, like headset or telephone handset.

TAPI Call sequence

To write a TAPI application, we should start by finding out what's going on in the real world. What happens when we make a call, or when telephone rings, when the user presses a button, caller id comes,... .

With the events in our hand we can make a logic to write our application. In addition to these series of events we shall consider the sequence of calls to TAPI functions that is needed to start our event driven application.

A simple sequence for an answering machine would be like this:

   - Init a line/phone. (Now you know what a line/phone is!)
   - Open a line/phone.
   - Create an event receiving mechanism.(Run a thread for example, to receive events)
   - Do Telephony interactions.
   - Shutdown and clean up.

each sequence has it's own details(subsequences) and calls to TAPI functions. Main calls listed here:
Init:
   - lineInitializeEx
   - lineNegotiateAPIVersion
   - lineGetDevCaps

Open:
   - lineOpen
   - lineSetNumRings
   - lineSetStatusMessages

Creating an event thread
  
does not need any more calls to TAPI functions

ShutDown & Cleanup
   - lineDrop
   - lineClose
   - lineDeallocateCall
   - lineShutdown

Seems to be easy? well, it's not that easy! Each call needs it's true inputs, beside, all outputs must be checked. However a true output does not show a job well done(Asynchronous calls), but a wrong one shows an unsupported operation. There are other more important things either, like handling memory allocation and de allocation plus handling events and giving true responses, managing sound playback & ... .

What I place here in CP, makes it a bit easier to do the job, but it is very limited and does not help you if you don't like to learn any thing! If you are seeking a library that does the whole job for you, this article and sample is not for you. 

TAPI Events

As I mentioned already, one important job is to create an event thread to process TAPI events, But which events? there are a lot of TAPI events. I created an event thread and made a call to the application, these are events I received and had to process:

Event wParam(1/2) How to process
LINE_APPNEWCALL A handle to a call is available
LINE_LINEDEVSTATE LINEDEVSTATE_RINGING Ringing, please pick up the phone!
LINE_CALLINFO LINECALLINFOSTATE_CALLERID CallerID
LINE_CALLSTATE LINECALLSTATE_CONNECTED A connection established
LINE_MONITORDIGITS Type of DTMF The other party pressed a digit
LINE_REPLY No! probably an error occurred.
LINE_CALLSTATE LINECALLSTATE_DISCONNECTED The remote party has disconnected from the call

Sound playback

Note: I'll not explain or implement sound recording!

Sound playback is one of the points you would be very careful using it. Some critical errors might occur here. There are a lot of considerations like sound format, quality, buffering and unbuffering truly and on time, ... . In fact writing a great sound class or library will be the second (or even 1st) time-consuming job you'll probably do.

There are some considerations:
   Selecting a sound quality depends on your modem capabilities. Today almost all types of voice modems support 'Wave PCM 8.000 KHz; 16 bit; Mono', which is the best possible format! because this is the best quality(8 KHz voice) that real phone lines and cables and communication tools (at least in my country), are capable to transform. But this quality is almost bad! Volume is low, so we'll have to record our voice a bit louder, so that at the other side it turns to an acceptable one. Try it yourself. perhaps for a rest!

However Microsoft says the true format is  8.000KHZ 8 bit mono CCITT u-law (Microsoft Knowledge Base Article ID: Q142745).

Buffering is another issue. Read next part about the delay, it's interesting.

Solving delay problem

Delay is a problem for most TAPI applications that use unimodem! It happens when you need to process a DTMF.

When user presses a digit on her phone, Application must stop the sound playback, and probably play another or do something.
In order to stop playback of a sound now playing, we have to call waveOutReset API. MSDN says: "The waveOutReset function stops playback on the given waveform-audio output device and resets the current position to zero. All pending playback buffers are marked as done and returned to the application." which is true while we are talking about sound card. But my small experience with modems, mostly internal soft modems, taught me that this API, when called, does this: "Waits until a given buffer played completely, then returns"! but it was very bad.
My files where not that big, at last 100 KB, so no need to double buffering, but then the result was clearly very bad!
It was a great issue, user pressed a digit, waits for a response but last sound is playing yet, and she presses more digits, and .... So I had to find a solution, and I almost did!

I used a new double-buffering scheme. This way: I imported all the sound data to a buffer into memory(!) and then logically divided it to some very small parts. I then used a variable to store base address, and gave the address with my logical small bufferSize to waveOutWrite API, upon DONE message for the small chunk, I gave the API "base-address + small size". So this way I don't need to reload the chunk from File and I can minimize the chunk size even to 1 K, without any problem. And now if the user presses a digit, waveOutReset continues the (very)small sized data which is almost nothing. The result was even better than I thought. It was not very good on sound card, but It was great on modem. The problem almost solved Smile | :) almost because I was always using 16 bit files. When I used 8bit, I encountered some problems. Perhaps problem is with my modem. I’ll try to solve this problem either.

Caller ID Junk!

Why did I add a topic for caller id? well, just because most of my headaches was about it! while many modems were able to support voice, many many more were unable to detect CallerID. The sample code here is capable of detecting that if everything is all right.

I should mention here that most V.92 modems were good at finding CallerID.

What I'm going to describe here is not the software part in TAPI to do that. it is straight. But I'm going to say some of the most common problems in detecting CallerID. If you see a label on your modem saying CallerID supported but even the application inside the CD you received with your modem does not work, then this part is yours.

Caller ID is something really local! It all depends to your local telephone company, to select a technology and use it. The most common technologies are DTMF and FSK.
 First time I heard DTMF, It confused me a lot. I thought 'Was it not the keys pressed by the person at the other party?' well that's all right!

DTMF stands for Dual Tone Multi-Frequency. 'DTMF assigns a specific sound frequency, or tone, to each key so that it can easily be identified by a monitoring microprocessor. That frequency is then translated into a usable analog or digital signal. This is commonly known as Touch Tone.'. This was what my dictionary says. Very funny that some telecommunication companies send CID in this format to us!
If you've ever pressed one of the keys of your phone (say #1), during a call to your friend, you've send a DTMF code to your friend! Just in that way the number will be send to you, mostly between the first and second rings of the phone.

FSK stands for Frequency Shift Keying, and I really did not understand the difference between the way it works and the way DTMF works! I've read something about a change in the domain in front of a change in the frequency! well, It does not matter, since I'm not a hardware engineer.

So, it is up to you to ask your local telephone company which system is active, and then try to find modems that support the mechanism in your area. I found some modems that support DTMF while most of them don't! Most of the modems support FSK.

There are also some hardwares that convert DTMF to FSK and sends them to the modem!! here:http://www.artech.com.tw/html/english/ex200/ex200.htm

I found on the web (excele tools, link can be found at the bottom of the article) that 'North America uses the Bellcore FSK Caller ID standard which sends the data between the first and second ring', and that ' In Europe, people have reported success with USR model number 5630'. well I don't know!! If you are in UK you might have some trouble!! Look at here :www.ainslie.org.uk/callerid/dev_soft.htm

If you have the right information about your area, and the appropriate modem, and bought it, there are still good reasons why you don't receive that information!

Check to see if you aren't picking up the phone too early, and before receiving Caller ID! Where I live for example caller ID comes between 1st and 2nd rings. If I set my TAPI application to pick up the phone on 1st ring, then I lost all the information! I'm not sure why It happens or if it happens all around the world, or not.

Can you trust your modem manufacturer, or their driver developers?!!
while many modems support caller ID, there are mistakes in their .inf file which may result our failure in detecting through TAPI. You can test it:

In Windows 2k and Xp there's a modem logger which will help us alooooooooot.

In XP goto Control Panel and find 'Phone And Modem Options' Open it, goto middle tab (Modems) then press 'properties' button, In the 'diagnostics' tab check the 'Append to log' checkbox, press 'View log' It'll open the log file in a notepad window delete all and save it close notepad, and OK the dialog.
Now use a software and listen to the line. Hyperterminal would be a good choice, if you don't use COM port now! Then call. after calling to the number connected to your computer, press 'View log' again to see if there is any phone number inside the log.

If you don't see your number try the second test below:

Open Hyper terminal in windows, give a name to your connection and select a com port instead of the modem name you see in the next dialog box of HyperTerminal. If your modem is located on 3rd COM for example, select COM3, then press Ok. Now call the line connected to your computer using another line(your cellphone perhaps). you must see 'Ring' 'Ring'... between rings if any data comes through that port you'll see that. If you don't see any thing, then your modem does not support CallerID, probably. Call the manufacturer if you can.

What does these test show us?
Many times there is a mistake in the .inf file came with your modem driver that you can solve it! and prevents you from receiving true CallerID. If your modem detects Caller ID then you must be able to see it when you listen to the port, or when your modem logs it. If you can see the number there, but unable to get it through your application, the problem is .inf file. How? In the FSK format a string comes before the number through the port. If modem detects the string, TSP will inform TAPI and you'll receive true message, if not you loose the data, but any application listening to the port directly will receive it! (Like Hyper Terminal). The string is 'NMBR = ' while in many .inf files is written 'NMBR='. Some blank spaces now are very invaluable! You can correct the .inf file and reinstall your modem driver. Thanks to the people describing this problem.(However it was not mine!)

If non of the tests give you the number, call the manufacturer as quickly as you can, or give the modem back, or change it (What I did many times!! but the manufacturer's did not answer and I lost some money testing modems  )
More information:
http://www.talkingcallerid.com/ModemDriver.htm
http://www.caller-id-answers.info/index.htm
http://www.ainslie.org.uk/callerid/cli_faq.htm
http://www.repairfaq.org/filipg/LINK/F_CallerID.html
http://www.exceletel.com/support/hardware/VoiceModems/

Using the code

Finally we are here. What do I offer?

What I have here is a template(Not C++ templates!) like sample. The sample does :

   - Finds appropriate Hardware(If exists at all!).
   - Inits, opens and listens for messages relative to answering machine.
   - detects CallerID and DTMF and Inform you.

To use this sample you have to add below classes to your project:

1 - Add TapiObj.cpp, TapiObj.h. This will attach CLine,CPhone and CTapiObj classes to your project.
2 - Add HSound.cpp, HSound.h. This will add Sound class(HSound).
3 - Add helper classes: HErrLogger, HDevices, HPlayList, HSettings. It is up to you yourself to complete or change these classes. These are really incomplete, specially HErrLogger. You can write your own version or modify these classes or remove them from the code!
4 - Finally add and modify the boss: HCentralManager. All you need to do here is to edit the CentralManager and call your own functions in the main message processor function ProcessMessage(...)

You see, this is why I say it's like a template that you have to place your code inside. As I mentioned earlier, I did not create this sample to be used without understanding (In that case I could create an ActiveX control) But I think CodeProject is not for such a thing.
Further more, the sample uses a special architucture, that I think makes working with TAPI a lot easier and quicker, But is not a good Object Oriented approach(I think). Take a look at this picture:

Sample Image - maximum width is 600 pixels

As you can see here, I created a group of managers, each doing a special job and contacting a coordinator, CHCentralManager, through a message thread. You'll be able to handle all events easily in central manager and inform other classes.(Like UI classes)

Why a separate thread for Sound and line and main processor? The reason is clear, I don't want to engage LineEvent to operations because more important message might come, like Disconnect, that has to be processed even before a playlist is fully played. So when receiving a new message, I Post a message to another thread and the main TAPI message loop continues immediately, ready to process new messages Smile | :)

How components gather to shape the application:
When you press the start button in main Dialog, an object of the type HCentralManager called m_managerwill be initiated by setting it's parent to this. Then we ask it to WakeUp. That's all you need to run the big brother!!

    // Initiate and start CentralManager
    m_manager.SetParent(this);
    m_manager.WakeUp();

Now What happens in m_manager:
WakeUP member function starts 3 thread that are responsible for message communication between CHLine,CHSound and the manager CHCentralManager. Then we ask the line and phone to Start . Remember the Line I described earlier? consider Line Object, an abstract of the real hardware line. All I want to do is to Initialize and open a real hardware line.
That's it! Now Line is open and ready for a user to call it, then core class CHLine will detect the line new status and sends us appropriate messages.
             /* Waking up Message Thread */
    m_pThread = AfxBeginThread(
        MessageThread,
        (LPVOID)this,
        THREAD_PRIORITY_NORMAL,0,NULL,0);

    ...

            /* Statr the line operations */
    m_line.Start();
               /* Statr the phone device */
    m_phone.Start();

Let's get to main function of the central manager, where's a station for all messages: ProcessMessage function.
void CHCentralManager::ProcessMessage(MSG msg)
{
    switch (msg.message) {

    case WM_DTMF:                   /* New DTMF code pressed by user */
        if (m_pWParent)
            m_pWParent->PostMessage(
                WM_STATUS_CHNGD,msg.lParam,ST_DTMF);
        break;
            /* Playlist playback is done & we have to terminate session */
    case WM_SOUND_DONE:                            
        break;
                    /* Line status changed, do appropriate job. */
    case WM_STATUS_CHNGD:
                                                
        switch (msg.lParam) {
        case ST_INIT: break;
        case ST_OPEN: break;
        case ST_RESTART: break;
        case ST_READY: break;
        case ST_BUSY: break;
        case ST_SHUTDN: break;
        case ST_RING: break;
        case ST_PICKED_UP: break;
        case ST_FAILED: break;
        }
        break;
                           /* If users callerId received */
    case WM_CALLERID:
        g_phoneNumber = m_line.GetCallerID();
                                /* Inform parent */
        m_pWParent->PostMessage(WM_STATUS_CHNGD,0,ST_CALLER_ID);
        break;

    default : break;
    }
}
This has some slight modifications in real code. Before going to the code, please read the below part.

Known Issues

- There are memory leaks!(Not a lot). I think it does not happen frequently. I don't know if I was too busy, lazy or tired that I left it to be! I'll probably solve this in later uploads.

- I'm a beginner, so it is logical that this sample has a lot of bugs. I need an MVP's feedback to find out what I did! I hope some one come and help me improve this.

- I still have problem exiting 100% correctly (I'm not sure if I processed all messages correctly). I'll try to make it much better, but it'll be done more quickly and more correctly with the help of some professionals.

- Multiple buffering has some problem with 8bit files with some modems. It has a simple solution, read trouble shooting below.

Troubleshooting

If you're unable to run the sample app, you should first make sure your modem is a voice modem and if the true driver is installed. You may go to Device-Manager to see if you have a unimodem sound device installed.

You might need to find error and problem. Most TAPI errors will be logged (Incomplete part of the app) into a text file 'errors.log' check to see if anything logged there.

I use 16 bit PCM files, check to see if your modem support it, otherwise try 8 bit and set the buffer size at first lines of HSound a bit more than your largest file.(My buffering has some problems with 8 bit files with some modems)

After above, if there is any problem, you need to debug application. Sorry but there was a lot of works and I was too busy to complete error reporting.

Some Story and advice!!

5 months ago I received a request for a project that I had not been doing already! An application that had to answer phone calls from customers of a shop and sell them something. Application had to recognize customers regarding the CallerID received when they called. The shop had a great database of numbers and addresses in a server running MS SQL Server 2000. I had to authenticate users by calling a stored procedure.
I accepted the application (a great experience!) but the problems I encountered made me to place this part here:

For Those who has not been writing such an application already:

     - Request as much time as you can. Don't think about a small system! My project which is a really simple telephony application has at least 9000 lines of source code (Just a simple answering machine) and took me 3 months of learning and coding.(plus passing my final university exams!)

   - Check if you can find a good hardware. If you live in a place where are not able to go to a shop and ask for a modem that has some technical capabilities, or if the place where you live in, is full of fake hardwares, (Like where I live), Never Never Never ever think about writing such an application!  I recommend V92 modems, trouble are less with them.

   -If you have to use CallerID, then read the callerID part of this article and then go to your local phone company and talk to their technical support. Remember that (as much as I know) there is not any modem capable of finding all types of CallerID's, so you should find a modem that is compatible with your area(Sometimes it is really difficult).

   -Find the blue ferry! If there is any one around you that you can ask your questions from, don't loose time! Microsoft has MVP's for TAPI and NewsGroups that can help you. See links at the bottom of the article.

   - Don't buy a modem unless you're sure of what you need. Many modems support voice and DTMF but you can't place more than 1 of them on 1 machine! so check whether you need to support multi lines, CallerID, DTMF, Voice, phone options (if you need to switch between handset/headset/speaker and application).

   -There are better choices than modems and TAPI, some hardwares like dialogic boards. Try finding some information about them, and be sure these hardwares are very expensive and programming is much easier! manufacturer mostly has their own API sets. If you are going to do a multi line application, I recommend them first!
I found on the web that more than 70% of telephony systems don't work under Windows platform. I'm not sure if it's really true! and I don't know which system do they use, perhaps Linux, or ... .

My great wish

   - Working with a group, even 2 person, is a dream!! I had such an experience already, but my workmate had to move to another cityFrown | :( You just can't believe have much I asked the god to give me the opportunity to work with a group, again. 

Links and helps on the web:

Here is a list of articles and sample files and applications that lead me to figure out what is what! and place this article here.
www.microsoft.com/msj/0498/tapi.aspx An Answering machine with TAPI 2.1.
http://www.codeproject.com/internet/TAPISample.asp A great kick-start to me. Thanks a lot 'T.YogaRamanan'
There is also a TAPI 3.x version of the MSJ article and sample:www.microsoft.com/msj/1198/tapi3/tapi3.aspx
http://www.i-b-a-m.de/Andreas_Marschall's_TAPI_and_TSPI_FAQ.htm This is the best FAQ I think!, he also answers your questions in a newsgroup: Microsoft.public.win32.programmer.tapi
www.julmar.com/and it's very great samples and libraries.
www.allen-martin-inc.com/amtapitelephony.htm
www.rainyjay.com/tapi/tapi.htm
www.caller-id-answers.info/index.htm
http://project.uet.itgo.com/sapi.htm
www.exceletel.com/support/hardware/VoiceModems
<!------------------------------- That's it! --------------------------->
And a lot others. Thanks a lot to all their helps.

License

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

Share

About the Author

Hamed Mosavi
Architect
Iran (Islamic Republic Of) Iran (Islamic Republic Of)
Hi!
My name is Hamed. A man who started programming when he was very young and still likes programming a lot.

Comments and Discussions

 
Questionhi Pingroupzendehdel19-Jul-14 22:44 
AnswerRe: hi PinmemberHamed Mosavi21-Jul-14 0:46 
QuestionCan you Help Me? PinmemberArsenis10-Jan-13 6:50 
AnswerRe: Can you Help Me? PinmemberHamed Mosavi17-Jan-13 4:13 
QuestionBuild Problem PinmemberVet Ralph10-Nov-12 5:14 
AnswerRe: Build Problem PinmemberHamed Mosavi17-Nov-12 6:48 
AnswerRe: Build Problem [modified] PinmemberHamed Mosavi17-Nov-12 6:50 
Questionmy vote of 5 Pinmembermali_IT13-Jul-12 23:46 
Questionsalam PinmemberMember 81945286-Jul-12 4:25 
Questionhelp me Pinmemberhossain25526-Nov-11 6:23 
QuestionWitch modem is good? PinmemberMorteza_Developer29-Jul-11 2:35 
Questionدرخواست راهنمایی Pinmemberahadmehdi19-Jun-11 6:57 
Generalalternative solution sip sdk PinmemberAlejandro Bacha18-Oct-10 22:34 
Generalسلام [modified] Pinmemberostovarit14-Jul-10 1:06 
GeneralRe: سلام PinmemberHamed Mosavi14-Jul-10 2:40 
GeneralRe: سلام Pinmemberostovarit14-Jul-10 5:25 
GeneralRe: سلام PinmemberHamed Mosavi14-Jul-10 9:59 
QuestionCan you just help...Thanks.... PinmemberASUO147811-Jul-10 23:52 
AnswerRe: Can you just help...Thanks.... PinmemberHamed Mosavi12-Jul-10 5:00 
GeneralRe: Can you just help...Thanks.... PinmemberASUO147812-Jul-10 14:15 
GeneralRe: Can you just help...Thanks.... PinmemberHamed Mosavi12-Jul-10 17:53 
GeneralRe: Regarding your problems..... PinmemberASUO147813-Jul-10 14:52 
GeneralRe: Regarding your problems..... PinmemberHamed Mosavi13-Jul-10 20:46 
GeneralTAPI "BlindTransfer" Problem Pinmemberzendehdel21-Jun-10 23:04 
GeneralRe: TAPI "BlindTransfer" Problem PinmemberHamed Mosavi21-Jun-10 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
Web02 | 2.8.141030.1 | Last Updated 10 Sep 2006
Article Copyright 2006 by Hamed Mosavi
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid