Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

Understanding The COM Single-Threaded Apartment Part 2

, 18 Feb 2005 CPOL
Learn the fundamental principles of the COM Single-Threaded Apartment Model by code examples.
ccomthread2_src.zip
SimpleCOMObject1
SimpleCOMObject2
Test Programs
VBSTACOMObj
Shared
SimpleCOMObject1.def
SimpleCOMObject1.dsp
SimpleCOMObject1.dsw
SimpleCOMObject1.rgs
SimpleCOMObject1ps.def
SimpleCOMObject1ps.mk
SimpleCOMObject1.tlb
SimpleCOMObject2.def
SimpleCOMObject2.dsp
SimpleCOMObject2.dsw
SimpleCOMObject2.rgs
SimpleCOMObject2ps.def
SimpleCOMObject2ps.mk
SimpleCOMObject2.tlb
VCTests
VBTest
DemonstrateSTAInterThreadMarshalling
VCTest02
VCTest01
VCTest02.dsp
VCTest02.dsw
VBSTACOMObj.tlh
VBSTACOMObj.tli
VCTest01.dsp
VCTest01.dsw
SimpleCOMObject2.tlh
SimpleCOMObject2.tli
FormMain.frm
VBTest.exe
VBTest.vbw
VBTest.vbp
VBSTACOMObj.dll
VBSTACOMObj.exp
VBSTACOMObj.lib
VBSTACOMObj.vbw
VBSTACOMObj.cls
VBSTACOMObj.vbp
#define _WIN32_WINNT 0x0400
#include <windows.h>
#include <stdio.h>

#import "..\..\..\..\VBSTACOMObj\VBSTACOMObj.dll"
using namespace VBSTACOMObj;

#include "..\..\..\..\Shared\ComThread.h"





/* Simple function that displays the current thread ID. */
void DisplayCurrentThreadId()
{
  TCHAR szMessage[256];

  sprintf (szMessage, "Thread ID : 0x%X", GetCurrentThreadId());

  ::MessageBox(NULL, szMessage, "TestMethod1()", MB_OK);
}




/* LowLevelInProcMarshalInterface() uses low-level APIs       */
/* CreateStreamOnHGlobal(), CoMarshalInterface(), and IStream */
/* methods to perform interface pointer marshalling across    */
/* apartments in a process.                                   */
/*                                                            */
template <typename T>
LPSTREAM LowLevelInProcMarshalInterface(T* pInterface, REFIID riid)
{
  IUnknown*		pIUnknown = NULL;
  IStream*		pIStreamRet = NULL;

  /* QI the original interface pointer for its IUnknown interface. */
  pInterface -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);

  /* Once we get the IUnknown pointer, we serialize it into a stream of bytes. */
  if (pIUnknown)
  {
    /* Create a Stream Object which will reside in global memory.   */
    /* We set the first parameter to NULL, hence indicating that    */
    /* we want CreateStreamOnHGlobal() to internally allocate       */
    /* a new shared memory block of size zero.                      */
	/* The second parameter is set to TRUE so that when the returned*/
	/* stream object is Release()'d, the global memory will also be */
	/* freed.                                                       */
    ::CreateStreamOnHGlobal
    (
      0,
      TRUE,
      &pIStreamRet
    );

    if (pIStreamRet)
    {
	  LARGE_INTEGER li = { 0 };

      /* We use the new stream object to store the marshalling data */
      /* of spISimpleCOMObject2.                                    */
      /* The use of MSHCTX_INPROC indicates that the unmarshaling   */
      /* of the data in the stream will be done in another apartment*/
      /* in the same process.                                       */
      ::CoMarshalInterface
      (
        pIStreamRet,
        riid,
        (IUnknown*)pIUnknown,
        MSHCTX_INPROC,
        NULL,
        MSHLFLAGS_NORMAL
      );

      /* Always reset the stream to the beginning.*/
	  pIStreamRet -> Seek(li, STREAM_SEEK_SET, NULL);
    }

	pIUnknown -> Release();
	pIUnknown = NULL;
  }

  return pIStreamRet;
}





/* LowLevelInProcUnmarshalInterface() uses low-level APIs     */
/* CoUnmarshalInterface(), CoReleaseMarshalData(), and        */
/* IStream methods to perform interface pointer unmarshalling */
/* for an apartment in a process.                             */
/*                                                            */
template <typename T>
void LowLevelInProcUnmarshalInterface(LPSTREAM pIStream, REFIID riid, T** ppInterfaceReceiver)
{
  /* Deserialize the byte contents of the IStream object */
  /* into an actual interface pointer to be used only    */
  /* within this thread.                                 */
  /*                                                     */
  /* The interface pointer will not be a direct pointer. */
  /* It will be a proxy to the original pointer.         */
  if (pIStream)
  {
	LARGE_INTEGER li = { 0 };
    
    /* Make sure stream pointer is at the beginning of the stream. */
	pIStream -> Seek(li, STREAM_SEEK_SET, NULL);

    if
	(
	  ::CoUnmarshalInterface 
	  (
        pIStream,  //Pointer to the stream
        riid,     //Reference to the identifier of the interface
        (void **)ppInterfaceReceiver      //Address of output variable that receives the 
                   // interface pointer requested in riid
      ) != S_OK
	)
	{
	  /* Since unmarshalling has failed, we call   */
	  /* CoReleaseMarshalData() to destroys the    */
	  /* previously marshaled data packet contained*/
	  /* in pIStream.                              */
	  ::CoReleaseMarshalData(pIStream);
	}

    /* When pIStream is Release()'d the underlying global memory*/
	/* used to store the bytes of the stream is also freed.     */
	pIStream -> Release();
	pIStream = NULL;
  }
}





/* This thread function obtains a pointer to   */
/* a _ClassVBSTACOMObj interface via a stream  */
/* object which contains apartment-independent */
/* serialized bytes of the interface pointer.  */
/*                                             */
/* This set of bytes can be de-serialized into */
/* a proxy to the interface pointer.           */
/* This de-serialization (or unmarshalling)    */
/* process is performed by our own             */
/* LowLevelInProcUnmarshalInterface() function.*/
/*                                             */
DWORD WINAPI ThreadFunc_MarshalUsingLowLevelAPI(LPVOID lpvParameter)
{
  /* The IStream object may be passed from one thread */
  /* to another. It is thread-independent.            */
  LPSTREAM				pIStream = (LPSTREAM)lpvParameter;
  _ClassVBSTACOMObj*	p_ClassVBSTACOMObj = NULL;

  /* This thread enters an STA.*/
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

  /* Note the id of this thread.   */
  /* Let's say this is thread_id_2.*/
  DisplayCurrentThreadId();

  /* Deserialize the byte contents of the IStream object */
  /* into an actual interface pointer to be used only    */
  /* within this thread.                                 */
  /*                                                     */
  /* The interface pointer will not be a direct pointer. */
  /* It will be a proxy to the original pointer.         */
  if (pIStream)
  {
    /* pIStream will be Release()'d inside LowLevelInProcUnmarshalInterface(). */
    LowLevelInProcUnmarshalInterface<_ClassVBSTACOMObj>(pIStream, __uuidof(_ClassVBSTACOMObjPtr), &p_ClassVBSTACOMObj);
  }

  if (p_ClassVBSTACOMObj)
  {
    /* Call the TestMethod1() using the proxy.   */
    /* You will note that the thread id will     */
	/* not be thread_id_2. It will be threadid_1 */
	/* which is the id of the STA thread in which*/
	/* the object was originally created.        */
	p_ClassVBSTACOMObj -> TestMethod1();

    /* You may be surprised that the return value   */ 
	/* of Release() is actually zero, showing that  */
	/* it is the proxy (not the original interface) */
	/* that is Release()'d.*/
	p_ClassVBSTACOMObj -> Release();

	p_ClassVBSTACOMObj = NULL;
  }

  ::CoUninitialize();

  return 0;
}





/* This function demonstrates a proper way of marshalling an interface     */
/* pointer from one apartment to another.                                  */
/*                                                                         */
/* The method used here involves a stream of bytes that stores the thread  */
/* and apartment independent serialized bytes of an interface pointer.     */
/* This stream of bytes (headed by an IStream interface pointer) is then   */
/* passed to a thread of a destination apartment distinct from the original*/
/* interface pointer's apartment.                                          */
/*                                                                         */
/* The destination thread then deserializes the bytes into a proxy to the  */
/* original interface pointer.                                             */
/*                                                                         */
/* This demonstration uses low-level APIs to achieve its objectives as     */
/* coded in the functions LowLevelInProcMarshalInterface() and             */
/* LowLevelInProcUnmarshalInterface().                                     */
/*                                                                         */
void DemonstrateInterThreadMarshallingUsingLowLevelAPI(_ClassVBSTACOMObjPtr& sp_ClassVBSTACOMObj)
{
  HANDLE		hThread = NULL;
  DWORD			dwThreadId = 0;
  IStream*		pIStream = NULL;

  /* Prepare the serialization bytes of sp_ClassVBSTACOMObj   */
  /* and store it inside a stream object handled by pIStream. */
  pIStream = LowLevelInProcMarshalInterface<_ClassVBSTACOMObj>(sp_ClassVBSTACOMObj, __uuidof(_ClassVBSTACOMObjPtr));

  if (pIStream)
  {
    /* Demonstrate the use of a stream of bytes to marshal an interface */
    /* pointer from one thread to another. */
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL,
      (SIZE_T)0,
      (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingLowLevelAPI,
      (LPVOID)pIStream,
      (DWORD)0,
      (LPDWORD)&dwThreadId
    );

    ThreadMsgWaitForSingleObject(hThread, INFINITE);

    CloseHandle (hThread);

    hThread = NULL;

	/* Note : do not call pIStream -> Release().   */
	/* This is done in the receiving thread when   */
	/* it calls LowLevelInProcUnmarshalInterface().*/
  }
}





/* This thread function obtains a pointer to  */
/* an _ClassVBSTACOMObj interface via a stream*/
/* which contains an apartment-independent    */
/* serialized bytes of an interface pointer.  */
/*                                            */
/* This set of bytes can be de-serialized into*/
/* a proxy to the original interface pointer. */
/*                                            */
DWORD WINAPI ThreadFunc_MarshalUsingIStream(LPVOID lpvParameter)
{
  /* The IStream object may be passed from one thread */
  /* to another. It is thread-independent.            */
  LPSTREAM				pIStream = (LPSTREAM)lpvParameter;
  _ClassVBSTACOMObj*	p_ClassVBSTACOMObj = NULL;

  /* This thread enters an STA.*/
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

  /* Note the id of this thread.   */
  /* Let's say this is thread_id_3.*/
  DisplayCurrentThreadId();

  /* Deserialize the byte contents of the IStream object */
  /* into an actual interface pointer to be used only    */
  /* within this thread.                                 */
  /*                                                     */
  /* The interface pointer will not be a direct pointer. */
  /* It will be a proxy to the original pointer.         */
  if (pIStream)
  {
    ::CoGetInterfaceAndReleaseStream
	(
	  pIStream,
	  __uuidof(_ClassVBSTACOMObj),
	  (void**)&p_ClassVBSTACOMObj
	);
  }

  if (p_ClassVBSTACOMObj)
  {
    /* Call the TestMethod1() using the proxy.    */
    /* You will note that the thread id will      */
	/* not be thread_id_3. It will be thread_id_1 */
	/* (main()'s thread id) which is the id       */
	/* of the STA thread in which the object      */
	/* was originally created.                    */
	p_ClassVBSTACOMObj -> TestMethod1();

    /* You may be surprised that the return value   */ 
	/* of Release() is actually zero, showing that  */
	/* it is the proxy that is Release()'d.         */
	p_ClassVBSTACOMObj -> Release();

	p_ClassVBSTACOMObj = NULL;
  }

  ::CoUninitialize();

  return 0;
}





/* This function demonstrates a proper way of marshalling an interface      */
/* pointer from one apartment to another.                                   */
/*                                                                          */
/* The method used here involves a stream of bytes that stores the thread   */
/* and apartment-independent serialized bytes of an interface pointer.      */
/* This stream of bytes (headed by an IStream interface pointer) is then    */
/* passed to a destination thread of an apartment distinct from the original*/
/* interface pointer's own apartment.                                       */
/*                                                                          */
/* The destination thread then deserializes the bytes into a proxy to the   */
/* original interface pointer.                                              */
/*                                                                          */
void DemonstrateInterThreadMarshallingUsingIStream(_ClassVBSTACOMObjPtr& sp_ClassVBSTACOMObj)
{
  HANDLE		hThread = NULL;
  DWORD			dwThreadId = 0;
  IUnknown*		pIUnknown = NULL;
  IStream*		pIStream = NULL;

  /* QI the original interface pointer for its IUnknown interface. */
  sp_ClassVBSTACOMObj -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);

  /* Once we get the IUnknown pointer, we serialize it into a stream of bytes. */
  if (pIUnknown)
  {
    ::CoMarshalInterThreadInterfaceInStream
    (
      __uuidof(_ClassVBSTACOMObj),
	  pIUnknown,
	  &pIStream
    );

	pIUnknown -> Release();
	pIUnknown = NULL;
  }

  if (pIStream)
  {
    /* Demonstrate the use of a stream of bytes to marshal an interface */
    /* pointer from one thread to another. */
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL,
      (SIZE_T)0,
      (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingIStream,
      (LPVOID)pIStream,
      (DWORD)0,
      (LPDWORD)&dwThreadId
    );

    ThreadMsgWaitForSingleObject(hThread, INFINITE);

    CloseHandle (hThread);

    hThread = NULL;

	/* Note : do not call pIStream -> Release(). */
	/* This is done when receiving thread calls  */
	/* CoGetInterfaceAndReleaseStream().         */
  }
}





/* This is a thread start function that demonstrates */
/* the use of a Global Interface Table to transfer   */
/* interface pointers from one thread to another.    */
DWORD WINAPI ThreadFunc_MarshalUsingGIT(LPVOID lpvParameter)
{
  /* The cookie of the interface registered in the GIT is passed by */
  /* the thread parameter. */
  DWORD						dwCookie = (DWORD)lpvParameter;  
  _ClassVBSTACOMObj*		p_ClassVBSTACOMObj = NULL;
  IGlobalInterfaceTable*	pIGlobalInterfaceTable = NULL;

  /* Make this thread an STA thread. */
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

  /* There is a single instance of the global interface */
  /* table per process.                                 */
  /* Hence all calls in a process to create it will     */
  /* return the same instance.                          */
  CoCreateInstance
  (
    CLSID_StdGlobalInterfaceTable,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IGlobalInterfaceTable,
    (void **)&pIGlobalInterfaceTable
  );

  if (pIGlobalInterfaceTable)
  {
    /* Display the id of this thread. Let's say this is thread_id_4. */
    DisplayCurrentThreadId();

    /* Retrieve the interface pointer from the GIT. */
	/* What is returned is actually a proxy to the  */
	/* original interface pointer created in the    */
	/* main() function.                             */
	pIGlobalInterfaceTable -> GetInterfaceFromGlobal
	(
      dwCookie,
      __uuidof(_ClassVBSTACOMObj),
	  (void**)&p_ClassVBSTACOMObj
    );

    if (p_ClassVBSTACOMObj)
    {
	  /* Display the id of the thread which executes */
	  /* TestMethod1(). This should be thread_id_1.  */
	  /* That is, it is the id of main()'s thread.   */
	  p_ClassVBSTACOMObj -> TestMethod1();

      /* Release() the proxy interface pointer.         */
	  /* The current ref count of the proxy is returned.*/
	  /* This ref count may not tally with that of the  */
	  /* original interface pointer.                    */
	  p_ClassVBSTACOMObj -> Release();

	  p_ClassVBSTACOMObj = NULL;
    }

	pIGlobalInterfaceTable -> Release();
	pIGlobalInterfaceTable = NULL;
  }

  ::CoUninitialize();

  return 0;
}





/* This function demonstrates a proper way of marshalling an interface     */
/* pointer from one apartment to another.                                  */
/*                                                                         */
/* The method used here involves the Global Interface Table (GIT) which    */
/* is used to store interface pointers from various apartments in a process*/
/* -wide repository.                                                       */
/*                                                                         */
/* A thread of a destination apartment can obtain an interface pointer from*/
/* this table.                                                             */
/*                                                                         */
/* What the destination thread receives is actually a proxy to the original*/
/* interface pointer.                                                      */
/*                                                                         */
void DemonstrateInterThreadMarshallingUsingGIT(_ClassVBSTACOMObjPtr& sp_ClassVBSTACOMObj)
{
  HANDLE					hThread = NULL;
  DWORD						dwThreadId = 0;
  IUnknown*					pIUnknown = NULL;
  IGlobalInterfaceTable*	pIGlobalInterfaceTable = NULL;
  DWORD						dwCookie = 0;

  /* There is a single instance of the global interface table per process.    */
  /* Hence all calls in a process to create it will return the same instance. */
  /* We can get an interface pointer to this GIT here in this function. Later,*/
  /* another thread is able to retrieve the same GIT interface pointer via    */
  /* another call to ::CoCreateInstance().                                    */
  ::CoCreateInstance
  (
    CLSID_StdGlobalInterfaceTable,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IGlobalInterfaceTable,
    (void **)&pIGlobalInterfaceTable
  );

  if (pIGlobalInterfaceTable)
  {
    /* QI the original interface pointer for its IUnknown interface. */
    sp_ClassVBSTACOMObj -> QueryInterface (IID_IUnknown, (void**)&pIUnknown);

    if (pIUnknown)
    {
	  /* Register this interface pointer in GIT.    */
	  /* A cookie, identifying the interface pointer*/
	  /* is returned.                               */
	  /* No need to call pIUnknown -> AddRef().     */
	  /* Another thread can retrieve the pIUnknown  */
	  /* using the cookie.                          */
      pIGlobalInterfaceTable -> RegisterInterfaceInGlobal
	  (
        pIUnknown,
        __uuidof(_ClassVBSTACOMObj),
        &dwCookie
      );

	  pIUnknown -> Release();
	  pIUnknown = NULL;
    }
  }

  if (dwCookie)
  {
    /* Demonstrate the use of GIT to marshal an interface */
    /* pointer from one thread to another.                */
	/* The cookie of the interface pointer is passed as a */
	/* parameter to the thread function.                  */
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL,
      (SIZE_T)0,
      (LPTHREAD_START_ROUTINE)ThreadFunc_MarshalUsingGIT,
      (LPVOID)dwCookie,
      (DWORD)0,
      (LPDWORD)&dwThreadId
    );

    /* Get this thread to wait until the new thread ends.*/
	/* In the meantime, we must continue to process      */
	/* Windows messages.                                 */
    ThreadMsgWaitForSingleObject(hThread, INFINITE);

    CloseHandle (hThread);

    hThread = NULL;

    pIGlobalInterfaceTable -> RevokeInterfaceFromGlobal(dwCookie);

	dwCookie = 0;
  }

  if (pIGlobalInterfaceTable)
  {
    pIGlobalInterfaceTable -> Release();
    pIGlobalInterfaceTable = NULL;
  }
}





/* This thread function receives an interface pointer DIRECTLY from */
/* another thread. What we have is not a proxy but a DIRECT pointer */
/* to the original interface pointer.                               */
DWORD WINAPI ThreadFunc_DangerousTransferOfInterfacePointers(LPVOID lpvParameter)
{
  /* Make this an STA thread. */
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

  /* Display the id of this thread. */
  /* Let's say this is thread_id_5. */
  DisplayCurrentThreadId();

  if (1)
  {
    /* Directly cast the thread parameter into */
	/* a _ClassVBSTACOMObj interface pointer.  */
    _ClassVBSTACOMObj* p_ClassVBSTACOMObj = (_ClassVBSTACOMObj*)lpvParameter;

	/* A crash will occur here when TestMethod1() executes. */
	p_ClassVBSTACOMObj -> TestMethod1();

    /* Calling Release() will affect the original object's */
	/* reference count.                                    */
	p_ClassVBSTACOMObj -> Release();

	p_ClassVBSTACOMObj = NULL;
  }

  ::CoUninitialize();

  return 0;
}





/* This function demonstrates an illegal and dangerous method of transferring */
/* an interface pointer from one apartment to another.                        */
/* This method will work for an ATL generated COM DLL Server AND a client app */
/* deliberately developed to avoid threading problems.                        */
/* But for a COM Server created via Visual Basic, a crash will occur when a   */
/* method of the interface is invoked.                                        */
void DemonstrateDangerousTransferOfInterfacePointers(_ClassVBSTACOMObjPtr& sp_ClassVBSTACOMObj)
{
  HANDLE	hThread = NULL;
  DWORD		dwThreadId = 0;

  /* We are going to DIRECTLY pass an _ClassVBSTACOMObj interface  */
  /* pointer to a thread via thread parameter.                     */
  /* We need to AddRef() the interface pointer before passing it   */
  /* to a client thread. The client thread must later Release() it.*/
  sp_ClassVBSTACOMObj -> AddRef();

  hThread = CreateThread
  (
    (LPSECURITY_ATTRIBUTES)NULL,
    (SIZE_T)0,
    (LPTHREAD_START_ROUTINE)ThreadFunc_DangerousTransferOfInterfacePointers,
    (LPVOID)((_ClassVBSTACOMObj*)sp_ClassVBSTACOMObj),
    (DWORD)0,
    (LPDWORD)&dwThreadId
  );

  ThreadMsgWaitForSingleObject(hThread, INFINITE);

  CloseHandle (hThread);

  hThread = NULL;
}





/* This program aims to demonstrate the transfer of an interface pointer
   across apartments. There are 4 methods shown :
   1. By way of IStream using low-level APIs.
   2. By way of IStream using high-level APIs.
   3. By way of the Global Interface Table (GIT).
   4. By an incorrect and dangerous way.
*/
int main()
{
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

  /* Display the current thread id.*/
  /* Let's say this is thread_id_1.*/
  DisplayCurrentThreadId();

  if (1)
  {
    _ClassVBSTACOMObjPtr sp_ClassVBSTACOMObj;

	/* Instantiate coclass ClassVBSTACOMObj     */
	/* and get its _ClassVBSTACOMObj interface. */
    sp_ClassVBSTACOMObj.CreateInstance(__uuidof(ClassVBSTACOMObj));

    /* Call its TestMethod1() method. Note the thread id. */
	/* This should be thread_id_1.                        */
    sp_ClassVBSTACOMObj -> TestMethod1();

	DemonstrateInterThreadMarshallingUsingLowLevelAPI(sp_ClassVBSTACOMObj);

	DemonstrateInterThreadMarshallingUsingIStream(sp_ClassVBSTACOMObj);

	DemonstrateInterThreadMarshallingUsingGIT(sp_ClassVBSTACOMObj);

	DemonstrateDangerousTransferOfInterfacePointers(sp_ClassVBSTACOMObj);

    /* Call its TestMethod1() method again.          */
	/* The thread id displayed is still thread_id_1. */
	sp_ClassVBSTACOMObj -> TestMethod1();
  }

  ::CoUninitialize();

  return 0;
}





By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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

Share

About the Author

Lim Bio Liong
Web Developer
Singapore Singapore
Lim Bio Liong is a Specialist at a leading Software House in Singapore.
 
Bio has been in software development for over 10 years. He specialises in C/C++ programming and Windows software development.
 
Bio has also done device-driver development and enjoys low-level programming. Bio has recently picked up C# programming and has been researching in this area.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.1411019.1 | Last Updated 19 Feb 2005
Article Copyright 2005 by Lim Bio Liong
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid