Click here to Skip to main content
15,867,704 members
Articles / Programming Languages / C++
Article

Adding automation to MFC applications

Rate me:
Please Sign up or sign in to vote.
4.80/5 (56 votes)
6 Sep 200418 min read 221.5K   5.6K   134   42
Step-by-step instructions on how to add OLE automation to an already existing application. In addition, it illustrates how to do things without using the code as provided by the application wizard.

Table of contents

Introduction

Automation support in MFC is a great example of why some people don't like MFC: it works great as long as you follow the pre-made examples and use MFC exactly like it was designed, but as soon as you get even a little bit off the beaten path, you're on your own. There are some tutorials in MSDN and articles online that will teach you how to use the 'New project' wizard to generate an automation-enabled application, and some of those even explain some of the working of the code the wizard generates - but unfortunately, that's not always how things go in the Big Bad Real World. Sometimes, you inherit an application that is so old that the initial skeleton was probably generated with the wizard of Visual Studio 4; that has been expanded and hacked on by dozens of people, each with their own understanding (or lack thereof) of the MFC doc/view architecture; that has mutated from an afternoon-hack-proof-of-concept to a piece of software that is critical for the survival of the company; that has had so much surgery that its innards have started to resemble (metaphorically speaking) the leftovers of the struggle that a 3-year old had with a big bowl of spaghetti bolognese. Believe me - not pretty.

So, what can you do when you are asked to add automation support to such an application? Starting over with a fresh application and moving the functionality of the old code back in is not an option. And even if it was, you would find out that the wizard-generated code is built for the idea that the central element of your application's object model is the Document. In some MFC applications, that is simply not the case any more. The only recourse is to figure out exactly what it is that makes automation tick in an MFC application - and add those pieces into the old application.

The obvious way to start is to take an application that is wizard-generated with automation support, and compare it to an application that doesn't have automation support. Add those differences to the old application and you're all set, right? Well, that's partially true - as long as the object model that you want to implement is centered around a Document, as I said before. If you have an object model that doesn't work this way, and you wonder how to get automation to work, read on.

Automation

"But wait", you say, "what is this 'automation' thing you've been talking about?". Ah yes, esteemed reader, excuse me for not elaborating on this earlier. Automation is, simply put, a way for your application to interact with Visual Basic, VBScript, JavaScript and, indeed, any other language that can work with COM objects. Which reveals how automation is implemented: as a COM interface. Which also reveals that in order for your application to support automation, it will have to support COM. It would be outside the scope of this article to give a complete primer on COM (and indeed for a complete primer on automation as well); I refer to the section 'References' for further reading for those who do not have at least some notion of what COM is and how it works. I will suppose from now on that you know what COM and automation are; that you know what an 'object model' is, and that you have one (or at least have an idea of what it will look like) for the application you wish to automate; and that you know about MFC's Doc/View architecture. You don't have to know a whole lot about that last one though, since the biggest part of this article is about how to *not* use it.

Although I said that you can use automation from any language, there is a little nuance to to be made to that statement. For scripting languages to access a COM object, they would need to have a description of the interface of the object. That description can be read from a type library (.tlb file) but not all scripting languages have access to that. Therefore, there is a way to query an object for the methods it provides: implementing the IDispatch interface. But for languages who *can* read a tlb, there should be a way to do that, too. The solution is simple: a dual interface. Again, for details on the theory on dual interfaces, see the section 'References'; I'm bringing it up here because I will assume that you want your objects to have a dual interface.

The Problem

By now, being the interested reader you are, you've looked up 'automation' in MSDN, and you've seen a wealth of articles that explain the concept of automation in MFC and how to use the class wizard to add automation-enabled classes. So, you're probably wondering "Why am I reading this - I can get this same information from MSDN?". You see, the problem is that these articles are based on the following (implicit) assumptions:

  • You generated your application skeleton with the 'enable automation' option on.
  • Your object model revolves around a Document object.
  • You want a dispinterface, not a dual interface.

No documentation gives a step-by-step overview of what you have to do to automate an existing application, detailing what everything does, and the considerations to take into account. That is what this article tries to remedy.

The Solution

This article, of course! Below, I will present the steps to take to make your application scriptable from every COM-enabled language. It turns out that the changes you have to make can be divided in these groups:

  • Defining your object model in IDL.
  • Implementing your object model as CCmdTarget-derived classes.
  • The general initialization. Initializing COM, registering your objects with the system.

I've also included a small section on how to start your automated application from C++ and VBScript for writing small test clients.

I will present all steps in a tutorial-style way, and explain in the process what those steps do and what they are for. The included sample project contains a vanilla MFC multiple-document application, except for the minimal changes necessary to get automation to work. Those changes are clearly marked in the code.

To keep the code in this article to a minimum, I will only add one COM object. The name of the application that I'll automate is 'MyCoolApp', and the COM object that will be accessible from the outside will be a generic object named 'Application' with one method: Show(), which will, unsurprisingly, show the window. I find this to be a good first method to implement since automated applications will not be shown by default when they are instantiated from an automation client. When you are developing your application, you can call the Show() method, and when your application shows up, you know that the automation works.

What to do in your application

IDL file

The first step is to add a file that has the description of the COM objects. In the Solution Explorer, right-click on your application and select 'Add' -> 'Add New Item'. Choose 'MIDL file' and type in a file name, like 'mycoolapp.idl'. This will add the file to your project and open it. As mentioned before, we'll make an object named 'application' with one method: Show(). The IDL is mostly boilerplate code. The only thing you must remember if you choose to copy this sample is to change the GUIDs. A GUID is a globally unique identifier for your interfaces; if you copy the GUIDs below, they may conflict with another application that also uses them! This may or may not pose a problem in the future, but to be sure, change them! It's easy to generate a GUID: in Visual Studio, click 'Tools', 'Create GUID', and there you go. Click the 'Copy' button to copy the newly generated GUID to the clipboard.

That being said, here is the code:

#include "olectl.h"
[ uuid(526E36B7-F346-4790-B741-75D9E5B96F4B), version(1.0) ] // This is usually the name of your application.
library mycoolapp
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

    [ uuid(6263C698-9393-4377-A6CC-4CB63A6A567A),
      oleautomation,
      dual
    ]
    interface IApplication : IDispatch
    {
        [id(1), helpstring("method Show")] HRESULT Show (void);
    };

    [ uuid(9ACC7108-9C10-4A49-A506-0720E0AACE32) ]
    coclass Application
    {
        [default] interface IApplication;
    };
};

Some points of attention:

  • you need to have 3 different GUIDs
  • you need to have a coclass which will be named after the object you're modeling
  • you need an interface definition which is (by convention) named 'I' + the name of the object you're modeling.

Automated class header

Now, we'll add a class that represents the object that will be automated (that is, that can be accessed by automation clients). This is, your client's entry point into your application (or one of the entry points if you implement multiple interfaces). Adding it is fairly straightforward, I'll walk you through it:

Add a class and name it after your interface, 'Application' in this case. I'll stick with the MFC naming and call it CApplication for now (although I hate the C prefix personally - see References). You can do this with the wizard or add a class by hand. Derive it from CCmdTarget. Add in #include statement (I'll explain later where this comes from):

#include "mycoolapp_h.h"

Add the following functions:

virtual void OnFinalRelease()
{
  CCmdTarget::OnFinalRelease();
}

HRESULT Show()
{
  AfxGetApp()->m_pMainWnd->ShowWindow(TRUE);
  return TRUE;
}

This is the place where you would place any cleanup code for your object. We have a simple example, there is no cleanup needed; we just call the parent.

Add the following macros in the header:

DECLARE_DYNCREATE(CApplication)
DECLARE_MESSAGE_MAP()
DECLARE_OLECREATE(CApplication)
DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()

These macros set up some members and functions that are needed to register the class with the system and to route calls to the COM object to your (C++) object.

Add an enum:

enum 
{
  dispidShow = 1L
};

In this enum, you need to have an entry for every function you add to your interface. In this example, there is only one, Show(), so there is only one entry in the enum. You can make up your own names here - later, we'll see where those names are referenced.

Next, declare an interface map and put in entries for every function you want your automation object to have. This sounds complicated but it's just a simple macro and some cut and paste:

BEGIN_INTERFACE_PART(LocalClass, IApplication)
    STDMETHOD(GetTypeInfoCount)(UINT FAR* pctinfo);
    STDMETHOD(GetTypeInfo)(
        UINT itinfo,
        LCID lcid,
        ITypeInfo FAR* FAR* pptinfo);
    STDMETHOD(GetIDsOfNames)(
        REFIID riid,
        OLECHAR FAR* FAR* rgszNames,
        UINT cNames,
        LCID lcid,
        DISPID FAR* rgdispid);
    STDMETHOD(Invoke)(
        DISPID dispidMember,
        REFIID riid,
        LCID lcid,
        WORD wFlags,
        DISPPARAMS FAR* pdispparams,
        VARIANT FAR* pvarResult,
        EXCEPINFO FAR* pexcepinfo,
        UINT FAR* puArgErr);
    STDMETHOD(Show)(THIS);
END_INTERFACE_PART(LocalClass)

"But Roel", you'll ask, "what the hell is all of that?", and I'll tell you: GetTypeInfoCount(), GetTypeInfo(), GetIDsOfNames() and Invoke() form the implementation of IDispatch, and Show() is the implementation of the method in our interface. If you want to know exactly what the first four do, I'll refer you to MSDN, but believe me, you're better off copying and pasting - that's what I did. For those wondering if all that standard stuff cannot be wrapped in a macro: see at the end of this article. The MFC ACDual example provides such macros.

Automated class implementation

Now, it's time for the implementation of the class that we just wrote a header for. Start with the MFC macro to implement dyncreate:

IMPLEMENT_DYNCREATE(CApplication, CCmdTarget)

Next, implement the constructor and the destructor, and add the following statements to them besides your own code:

  • EnableAutomation() and ::AfxOleLockApp() in the constructor
  • ::AfxOleUnlockApp() in the destructor

Then implement the message map:

BEGIN_MESSAGE_MAP(CApplication, CCmdTarget)
END_MESSAGE_MAP()

Now comes the interesting part: the dispatch map. A dispatch map looks a lot like a message map, in that it has a BEGIN_DISPATCH_MAP part, entries for every function that your interface has, and ends with a END_DISPATCH_MAP macro. This example will show the dispatch map for our simple interface with only one method:

BEGIN_DISPATCH_MAP(CApplication, CCmdTarget)
  DISP_FUNCTION_ID(CApplication, "Show", dispidShow, Show, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()

The arguments to the BEGIN_DISPATCH_MAP macro are the same as those to the message map: the name of the class and the name of the class it is derived from. The arguments to DISP_FUNCTION_ID are more interesting. The first one is (again) the name of the class you're implementing. The second one is a short string description of your method. It will generally be the same as the name you use in your class. The third one is a unique number that we've set up in an enum in the class declaration. This number has to be unique, that's why an enum is convenient here (and of course, also because it allows you to work with descriptive names instead of numbers). The next argument is the name of the method of the class in which the interface method is implemented. As I mentioned, this is usually the same as the second argument (except for the quotes, that is).

The fifth argument then is the return type of the method. This is not as you would expect VT_HRESULT but rather VT_NONE for all methods. It would take us too far to explain here why that is exactly, but in a few words: all methods of a dispinterface return HRESULTs, as reflected by the return type of the Show() method. That HRESULT, however, is used for error reporting, not to actually return a value. As such, when you call Show() from, for example, Visual Basic, you don't get to see that HRESULT at all - if an error would occur, VB's standard error handling mechanism would kick in. Therefore, you should declare all functions in your dispatch map as returning VT_NONE. The last argument to the DISP_FUNCTION_ID macro then is a space-separated list of the arguments that the function takes. Since ours doesn't take any arguments at all, we put in VTS_NONE. If it would have taken a string and an integer, we would have used "VTS_BSTR VTS_I2", for example. See MSDN for a full list of constants that are allowed here.

So far, for the dispatch map. On to the next map: the interface map. This is where the actual 'connection' between the COM object (your automated application) and your C++ object is made. Again, let's start with an example:

BEGIN_INTERFACE_MAP(CApplication, CCmdTarget)
  INTERFACE_PART(CApplication, IID_IApplication, LocalClass)
END_INTERFACE_MAP()

The first argument of the INTERFACE_PART macro is once again the name of the class we're implementing; the second argument is the interface name (most likely 'IID_' + the name of your interface), and the third argument is the first argument to the BEGIN_INTERFACE_PART macro (which we put in the declaration). Easy as that.

One more 'standard' implementation has to be done, using the IMPLEMENT_OLECREATE macro. Sample:

IMPLEMENT_OLECREATE(CApplication, "mycoolapp.Application", 
  0x9acc7108, 0x9c10, 0x4a49, 0xa5, 0x6, 0x7, 0x20, 0xe0, 0xaa, 0xce, 0x32)

The first argument is the name of the coclass as you specified it in the IDL file. The second name is the textual description by which your object will be known to, for example, Visual Basic; if you're not sure what to choose, make it '<library name>' + '.' + '<interface name>'. Look back at the IDL example and you'll see what I mean. The third parameter is very important: it's the GUID of the coclass you declared in the IDL file.

We're almost done here: add the implementation of the IDispatch members to the class:

STDMETHODIMP_(ULONG) CApplication::XLocalClass::AddRef()
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  return pThis->ExternalAddRef();
}
STDMETHODIMP_(ULONG) CApplication::XLocalClass::Release()
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  return pThis->ExternalRelease();
}
STDMETHODIMP CApplication::XLocalClass::QueryInterface(
  REFIID iid, LPVOID* ppvObj)
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  return pThis->ExternalQueryInterface(&iid, ppvObj);
}
STDMETHODIMP CApplication::XLocalClass::GetTypeInfoCount(
    UINT FAR* pctinfo)
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
  ASSERT(lpDispatch != NULL);
  return lpDispatch->GetTypeInfoCount(pctinfo);
}
STDMETHODIMP CApplication::XLocalClass::GetTypeInfo(
  UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo)
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
  ASSERT(lpDispatch != NULL);
  return lpDispatch->GetTypeInfo(itinfo, lcid, pptinfo);
}
STDMETHODIMP CApplication::XLocalClass::GetIDsOfNames(
  REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames,
  LCID lcid, DISPID FAR* rgdispid) 
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
  ASSERT(lpDispatch != NULL);
  return lpDispatch->GetIDsOfNames(riid, rgszNames, cNames, 
    lcid, rgdispid);
}
STDMETHODIMP CApplication::XLocalClass::Invoke(
  DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags,
  DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult,
  EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr)
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
  ASSERT(lpDispatch != NULL);
  return lpDispatch->Invoke(dispidMember, riid, lcid,
    wFlags, pdispparams, pvarResult,
    pexcepinfo, puArgErr);
}

Again, this is boilerplate code that can be simplified a lot using a few macros that are provided with the ACDual MSDN example. See the section near the end about that.

And finally, add the implementation of the Show() function. We just call the owner class' Show() method:

STDMETHODIMP CApplication::XLocalClass::ShowWindow()
{
    METHOD_PROLOGUE(CApplication, LocalClass)
    pThis->ShowWindow();
    return TRUE;
}

Changes to OnInitInstance

We also need to make some changes to OnInitInstance() to setup and register the COM objects with the system (in the registry). The first one is to call:

COleTemplateServer::RegisterAll();

The project wizard will put this call right after the ini files are loaded with LoadStdProfileSettings(); I suggest putting it there as well. It doesn't really matter, but it will look more familiar if/when you'd compare it to a wizard-generated application.

Then, scroll down a few lines until you see a call to ParseCommandLine(). Right after that, add the following code:

if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
{
    return TRUE;
} else if (cmdInfo.m_nShellCommand == CCommandLineInfo::AppUnregister){
    AfxOleUnregisterTypeLib(LIBID_mycoolapp);
} else {
    COleObjectFactory::UpdateRegistryAll();
    AfxOleRegisterTypeLib(AfxGetInstanceHandle(), LIBID_mycoolapp);
}

When your application is started from automation, it will be run with the parameters /Embedding or /Automation. In that case, we don't want to show the main window, so we return immediately. This means, of course, if you do want to show the main window, that you should call pMainFrame->ShowWindow(TRUE); before returning. The second 'if' tests whether your application was run with the /Unregserver or /Unregister switches. If it is, we remove all references to our application from the registry (actually, we let MFC do this for us). Finally, if we detect none of these switches, we let MFC put references to our COM objects into the registry. Yes, this means that every time your application is run, it is re-registered; this is to ensure that the registry is always consistent with the latest location of the executable.

That's all there is to it as far as the changes to OnInitInstance go. If you would generate an automation-enabled application with the wizard, you'd see more code here, more specifically a few calls to functions of an m_server object of type ColeTemplateServer. They are for the case that you want your Document to be automation-enabled; it is associated with a doctemplate so that a new document can be created when the automation server is started. Since this code is generated automatically with the wizard, I won't describe it here.

Resource

One final thing to do is to embed the type library in the resources section of your application. Go to the resource view, right-click on the resource file name, and choose 'resource includes'. At the bottom of the lower box, insert:

1 TYPELIB "<appname>.tlb"

where <appname> is the name of your application, of course (in our case, it would have been 'MyCoolApp.tlb'). This way, the resource compiler will embed the type library into the executable so that you don't have to distribute it separately.

Post-build step

This step isn't strictly necessary but will make your life easier: register your application every time it is build. It is simple: in the Properties of your project, add the following line to your post-build event:

"$(TargetPath)" /RegServer

Compiling your project

To get your project to compile, you need to link in a file that was generated by the MIDL compiler: mycoolapp_i.c. To do this, right-click on 'MyCoolApp' in the Solution Explorer, choose 'Add' -> 'Add Existing Item...', and select mycoolapp_i.c.

How to use your automation object

Of course, you want to test your new automated application. Remember that there are two ways to get to your objects: through 'regular' COM (or 'early binding', only for those environments that support it, like C++) and through IDispatch ('late binding', for scripting languages like VBScript). I'll demonstrate both methods here.

From C++

Let's start with a very simple C++ application. Make a simple dialog-based application with the class wizard, be it an MFC or an ATL/WTL application. Just make sure that :CoInitialize() and CoUninitialize() are called somewhere (that is done automatically in ATL applications). Put a button on the dialog somewhere, wire it up, and put the following in the message handler for the BN_CLICKED handler:

HRESULT hr;
hr = ::CoCreateInstance(CLSID_Application, NULL, 
     CLSCTX_LOCAL_SERVER, IID_IApplication, (void**)&m_IApplication);

if(SUCCEEDED(hr)) {
  if (m_IApplication) {
    m_IApplication->Show ();
  }
}

In the header for the dialog, declare a member like this:

IApplication* m_IApplication;

Now, all you need to do is include the file where IApplication is declared. It is automatically generated from the IDL file by the midl.exe IDL compiler, so you'll have to either copy it to the directory of your test application (which you'll have to do every time you change the IDL file) or construct a #include statement with a relative path in it. The file is named (by default) <coclassname>_h.h, so in our example, it is Application_h.h. When you go looking for this file, you'll notice another file: Application_i.c. This file contains the implementation of the interface and is needed by the linker. So, again, you can copy it, or add it to your project directly.

Now, build your application, click the button you've made, and voila - there is your application! Notice that if you take out the m_IApplication->Show(); line, you can still see your application being started by looking for it in the process list in the Windows Task Manager.

From VBScript

It's easier to start your application from VBScript. In three lines:

VBScript
Dim obj
Set obj = CreateObject("mycoolapp.Application ")
obj.Show ()

Notice that the argument that we pass to CreateObject is the name we passed in the IMPLEMENT_OLECREATE macro. Also, note that if you leave out the last line in one script, create a new script with all three lines, and run first the two-line script and then the three-line one, your application will be started only once! That means that if you call CreateObject() when an instance of your application is already running, a reference to that running instance will be returned.

Using the macros from the ACDual example

A lot of the code in this article is very standard: it will be exactly the same in every application you will ever automate. To avoid that, you can use the code in a header file that is provided with the ACDual example: mfcdual.h. The example itself can be found on MSDN; apart from some error handling code (which we didn't discuss in this article), it contains these macros:

  • BEGIN_DUAL_INTERFACE_PART: use this instead of BEGIN_INTERFACE_PART; it takes care of declaring the implementation of the IDispatch interface.
  • DELEGATE_DUAL_INTERFACE: add this one to your .cpp file to implement the functions that were declared with BEGIN_DUAL_INTERFACE_PART.

I strongly suggest you look at the way these macros are used in the ACDual example, and that you look at their contents; it will help you understand your application better when something goes wrong (notice that I didn't say 'if' but 'when').

References

Books on COM and automation basics:

Online resources:

History

4 Jul 2005 - updated download by Trevisan Andrea: The download code was reworked a little to include some important parts and ensure the solution file type is compatible with Microsoft Visual Studio.NET 2002 Academic.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Belgium Belgium
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRe: PPT automation when PPT is not installed in the machine Pin
roel_25-Aug-05 21:46
roel_25-Aug-05 21:46 
GeneralRe: PPT automation when PPT is not installed in the machine Pin
G200516-Sep-05 8:20
G200516-Sep-05 8:20 
GeneralMDI Application Pin
Tim Cook2-Jun-05 10:32
Tim Cook2-Jun-05 10:32 
GeneralRe: MDI Application Pin
Andy Bantly8-Apr-13 11:26
Andy Bantly8-Apr-13 11:26 
Generalconvert VC++ 5.0 project to VC++ 6.0 and I keep getting this error Pin
Member 164425025-May-05 3:32
Member 164425025-May-05 3:32 
GeneralVisual C++ 6.0 compatibility Pin
IlyaCher12-May-05 16:51
IlyaCher12-May-05 16:51 
GeneralRe: Visual C++ 6.0 compatibility Pin
Anonymous28-Jun-05 6:02
Anonymous28-Jun-05 6:02 
GeneralRe: Visual C++ 6.0 compatibility Pin
Anonymous28-Jun-05 6:04
Anonymous28-Jun-05 6:04 
GeneralVisual C++ 6 compatibility Pin
bljacobs27-Feb-05 9:20
bljacobs27-Feb-05 9:20 
GeneralRe: Visual C++ 6 compatibility Pin
bljacobs27-Feb-05 12:35
bljacobs27-Feb-05 12:35 
GeneralRe: Visual C++ 6 compatibility Pin
Danil Yelizarov23-Nov-09 9:30
Danil Yelizarov23-Nov-09 9:30 
GeneralVC++ Like Addin Framework Pin
thomas_tom9914-Oct-04 3:01
thomas_tom9914-Oct-04 3:01 
GeneralNo need to reimplement IDispatch Pin
Martin Richter [rMVP C++]16-Sep-04 1:17
Martin Richter [rMVP C++]16-Sep-04 1:17 
GeneralMy own solution Pin
Michal Mecinski7-Sep-04 10:55
Michal Mecinski7-Sep-04 10:55 
GeneralRe: My own solution Pin
Frank LiYang19-Sep-04 9:45
sussFrank LiYang19-Sep-04 9:45 
GeneralGood show! Pin
Victor Vogelpoel6-Sep-04 22:43
Victor Vogelpoel6-Sep-04 22:43 
GeneralRe: Good show! Pin
Digvijay Chauhan24-Sep-04 1:31
Digvijay Chauhan24-Sep-04 1:31 

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

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