Active Document Containers

Introduction.

This article explains how to create an MDI ActiveX
document container and drive it with help of scripts. Why should you bother
yourself with the ActiveX document containers? I don’t know. As for me, I
just wanted to use Microsoft Office programs seamlessly in my database client
programs. Most of my latest database client programs are MDI applications with
ActiveX scripting support. They are ActiveX script hosts. My applications have
their object model accessible with scripts. To create reports I use my own
report designer. But I can’t create so cool applications like Microsoft
Excel, Word, and Power Point. Microsoft is a big company and one programmer can
not do the same as many programmers do. So, I decided to use OLE automation
capabilities of Microsoft’s products in my own client applications. For
example, many excellent reports can be rendered with help of Microsoft Excel. Of
course, I could create Excel sheets with help of the “CreateObject”
function, but in this case the results would be placed in a separate from my
application window. I like the way Visual Studio works with documents. So I
decided do the same with my applications, and succeeded.


In the Release folder you will find a compiled program.
Run it. When the program starts, it will list all registered on your computer
document servers and ask you to create a document. Additionally, the program
works with script documents – plain texts that can be run. The program use
VBScript engine, but you can easily make it use any other engine. If you do not
see any document other then “Script document” in the
“New” dialog box, then you will not be able to test the program. In
this case create an ActiveX document server with help of the MFC class Wizard.
It would be nice if you had Microsoft Excel 8.0 installed on your machine. I
tested the program with Ms Office 8.0. It seems all is working fine, but I have
not tested the program with other versions of Office (newer or older) yet.

If you have registered document servers, you can create,
open, save, and print the documents in the same way the Visual Studio does with
ActiveX documents. Also, open the “DrivExcel.scr” file and run it
with help of the “File, Run” command. The script in this file should
open the “DriveExcel.xls” file (which is empty), fill it with some
values and create a diagram. I did not put in this sample a code to provide a
“CreateDocumentFile” function support, so that you would be able to
do something like the following:



Dim objSheet
Set objSheet = Document.CreateDocumentFile("Excel.Sheet.")
objSheet.Name="Market Share"

I have just put a code to open document files with
scripts. I think that having understood the sample you will add to your programs
this functionality yourself.

I think you should read Programming an Active Document Container in Internet Client SDK
before trying to understand my code.


 


First steps.



First I created a simple MDI OLE container with help of
the MFC Class Wizard. My next step was creation of a class, which would scan the
registry for installed ActiveX document servers. The name of the class is
“CAXDocInfoArray”. It is a simple array of pointers to the “
CAXDocInfo” structures.



struct CAXDocInfo
{
GUID m_clsid;//Class id of the document object
CString m_strDocStrings; //The same as m_strDocStrings in the CDocTemplate
};
class CAXDocInfoArray : public CTypedPtrArray&lt CPtrArray,CAXDocInfo*&gt
{
public:
~CAXDocInfoArray()
{
Clear();
}
void Clear();
void LoadFromRegistry();
};

With help of the “LoadFromRegistry” method
you can fill a CAXDocInfoArray with data about installed on your computer
ActiveX document servers.

Then I derived the “CAXDocContainerTemplate”
from the “CmultiDocTemplate” class to store CLSID of documents and
provide an access to protected m_strDocStrings member.



class CAXDocContainerTemplate : public CMultiDocTemplate
{
public:
GUID m_clsid;
CAXDocContainerTemplate(UINT nIDResource, CRuntimeClass* pDocClass,
CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass) :
CMultiDocTemplate(nIDResource, pDocClass,pFrameClass,pViewClass)
{
m_clsid=CLSID_NULL;
}
void SetDocStrings(LPCTSTR lpstrStrings)
{
m_strDocStrings=lpstrStrings;
}

LPCTSTR GetDocStrings() const
{
return m_strDocStrings;
}

DECLARE_DYNAMIC(CAXDocContainerTemplate);
};


Having done this I created a loop in the “
CAXDocContainerApp::InitInstance()“ to let MFC know about document
servers.



// Register the application’s document templates. Document templates
// serve as the connection between documents, frame windows and views.
CAXDocContainerDoc::m_arrayDocInfo.LoadFromRegistry();

for(int i=0; i&lt CAXDocContainerDoc::m_arrayDocInfo.GetSize(); i++)
{
CAXDocInfo* pInfo=CAXDocContainerDoc::m_arrayDocInfo[i];

CAXDocContainerTemplate* pDocTemplate;
pDocTemplate = new CAXDocContainerTemplate(
IDR_AXDOCCTYPE,
RUNTIME_CLASS(CAXDocContainerDoc),
RUNTIME_CLASS(CChildFrame), // custom MDI child frame
RUNTIME_CLASS(CAXDocContainerView));
pDocTemplate->SetContainerInfo(IDR_AXDOCCTYPE_CNTR_IP);
AddDocTemplate(pDocTemplate);

pDocTemplate->m_clsid=pInfo->m_clsid;
pDocTemplate->SetDocStrings(pInfo->m_strDocStrings);
}

CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_SCRIPTDOC,
RUNTIME_CLASS(CScriptDocument),
RUNTIME_CLASS(CChildFrame), // custom MDI child frame
RUNTIME_CLASS(CScriptView));
AddDocTemplate(pDocTemplate);


So, my program associated each ActiveX document with
“CAXDocContainerDoc” and “ “CAXDocContainerView”
classes. For each document a separate “CAXDocContainerTemplate” was
created. The objects of the “CAXDocContainerDoc” class know which
server to launch by the m_clsid member of the associated with it
“CAXDocContainerTemplate” object.

 


The ActiveX document container implementation.



The MFC Class Wizard created three classes:

a) CAXDocContainerDoc derived from COleDocument.


b) CAXDocContainerView derived from CView


c) CAXDocContainerCntrItem derived from
COleClientItem.

Most of the code I left without changes. The
COleClientItem does almost everything for document container support. I just
added one interface IOleDocumentSite to the interface map, and repeated the code
for IOleInPlaceSite support with a little modification to use my own frame
instead of ColeFrameHook (undocumented class, see MFC sources). The
implementation of the document site interface I took from the
“Framer” sample, which comes on the C++ compact disk.

When I created a CAXDocContainerCntrItem object and then
called DoVerb(0), the object was loaded and the IOleDocumnentSite::ActivateMe
was called.



STDMETHODIMP CAXDocContainerCntrItem::XDocumentSite::ActivateMe(IOleDocumentView *pView)
{
METHOD_PROLOGUE(CAXDocContainerCntrItem, DocumentSite)

CRect rc;
IOleDocument* pDoc;

/*
* If we’re passed a NULL view pointer, then try to get one from
* the document object (the object within us).
*/

if (NULL==pView)
{

if (FAILED(pThis->m_lpObject->QueryInterface(IID_IOleDocument, (void **)&pDoc)))
return E_FAIL;

if (FAILED(pDoc->CreateView(&pThis->m_xOleIPSite, 0, 0, &pView)))
return E_OUTOFMEMORY;

// Release doc pointer since CreateView is a good com method that addrefs
pDoc->Release();
}
else
{
//Make sure that the view has our client site
pView->SetInPlaceSite(&pThis->m_xOleIPSiteEx);

//We’re holding onto the pointer, so AddRef it.
pView->AddRef();
}

/*
* Activation steps, now that we have a view:
*
* 1. Call IOleDocumentView::SetInPlaceSite (assume done since
* either the view already knows, or IOleDocument::CreateView
* has done it already.
*
* 2. Call IOleDocumentView::SetRect to give a bunch of space to
* the view. In our case this is the whole client area of
* the CPages window. (Patron doesn’t use SetRectComplex)
*
* 3. Call IOleDocumentView::Show to make the thing visible.
*
* 4. Call IOleDocumentView::UIActivate to finish the job.
*
*/

pThis->m_pIOleDocView=pView;

//This sets up toolbars and menus first
pView->UIActivate(TRUE);

//Set the window size sensitive to new toolbars
pThis->GetActiveView()->GetClientRect(rc);
pView->SetRect(&rc);

//Makes it all active
pView->Show(TRUE);
return NOERROR;
}


The document (CAXDocContainerDoc) object has just one
client item object. It creates the one when opening a document or creating a new
document. Also the document object provides access to the embedded client object
for other C++ code.



BOOL CAXDocContainerDoc::OnNewDocument()
{
if (!COleDocument::OnNewDocument())
return FALSE;

// Create new item connected to this document.
CAXDocContainerCntrItem* pItem = NULL;

// TODO: add reinitialization code here
// (SDI documents will reuse this document)
CAXDocContainerTemplate* pTmpl=(CAXDocContainerTemplate*)GetDocTemplate();
if(pTmpl)
{
CWaitCursor wc;
TRY
{
pItem = new CAXDocContainerCntrItem(this);

// Initialize the item from the dialog data.
if (!pItem->CreateNewItem(pTmpl->m_clsid))
AfxThrowMemoryException(); // any exception will do

ASSERT_VALID(pItem);

return TRUE;
}
CATCH(CException, e)
{
if (pItem != NULL)
{
ASSERT_VALID(pItem);
pItem->Delete();
}
AfxMessageBox(IDP_FAILED_TO_CREATE);
return FALSE;
}
END_CATCH
}

return FALSE;
}
BOOL CAXDocContainerDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
// TODO: Add your specialized creation code here

// Create new item connected to this document.
CAXDocContainerCntrItem* pItem = NULL;

CWaitCursor wc;
TRY
{
pItem = new CAXDocContainerCntrItem(this);

m_bEmbedded=TRUE; //To avoid the assertion failed mesage

// Initialize the item from the dialog data.
if (!pItem->CreateFromFile(lpszPathName))
AfxThrowMemoryException(); // any exception will do

SetPathName(lpszPathName);
ASSERT_VALID(pItem);
return TRUE;
}
CATCH(CException, e)
{
if (pItem != NULL)
{
ASSERT_VALID(pItem);
pItem->Delete();
}

AfxMessageBox(IDP_FAILED_TO_CREATE);

}
END_CATCH

return FALSE;
}

CAXDocContainerCntrItem* CAXDocContainerDoc::GetDocItem()
{
POSITION pos=GetStartPosition();
if(pos)
{
CDocItem* pItem=GetNextItem(pos);
if(pItem->IsKindOf(RUNTIME_CLASS(CAXDocContainerCntrItem)))
return (CAXDocContainerCntrItem*)pItem;
}

return NULL;
}

BOOL CAXDocContainerDoc::OnSaveDocument(LPCTSTR lpszPathName)
{
// TODO: Add your specialized code here and/or call the base class
USES_CONVERSION;

WCHAR* wcPathName=T2W(lpszPathName);

IStorage* pStorage=NULL;
BOOL bSuccess=FALSE;
if(SUCCEEDED(StgCreateDocfile(wcPathName,STGM_READWRITE|STGM_SHARE_EXCLUSIVE|STGM_CREATE,0,&pStorage)))
{
IPersistStorage* pPersistStorage=NULL;
if(SUCCEEDED(GetDocItem()->m_lpObject->QueryInterface(IID_IPersistStorage,(void**)&pPersistStorage)))
{
if(SUCCEEDED(OleSave(pPersistStorage,pStorage,FALSE)))
{
pPersistStorage->SaveCompleted(NULL);
bSuccess=TRUE;
}

pPersistStorage->Release();
}

pStorage->Release();
}

return bSuccess;
}

Then I made some modifications of the
CAXDocContainerView object.


In the OnInitialUpdate the client item is activated with
the DoVerb method, and it’s document view rectangle is set.



void CAXDocContainerView::OnInitialUpdate()
{
CView::OnInitialUpdate();

// TODO: remove this code when final selection model code is written
CAXDocContainerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

if(GetDocItem())
{
GetDocItem()->DoVerb(0,this);

CRect rect;
GetClientRect(rect);
GetDocItem()->SetDocViewRect(rect);
}
}


When user resizes the frame the document view rectangle
is also set.



void CAXDocContainerView::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
CAXDocContainerCntrItem* pActiveItem = GetDocItem();
if (pActiveItem != NULL)
{
CRect rect(CPoint(0,0),CSize(cx,cy));
pActiveItem->SetDocViewRect(rect);
}
}

 


When user switches from one document to another we must
call the IOleInPlaceActiveObject::OnDocWindowActivate method. When user switches
to or off our application we must call
IOleInPlaceActiveObject::OnFrameWindowActivate method. This is done in the
OnActivateView.



void CAXDocContainerView::OnActivateView(BOOL bActivate, CView* pActivateView, CView* pDeactiveView)
{
CView::OnActivateView(bActivate, pActivateView, pDeactiveView);

if(GetDocItem())
{
IOleInPlaceActiveObject* pOIAObj=NULL;
if(SUCCEEDED(GetDocItem()->m_lpObject->QueryInterface(IID_IOleInPlaceActiveObject,(void**)&pOIAObj)))
{
if(pActivateView==pDeactiveView && pDeactiveView==this)
pOIAObj->OnFrameWindowActivate(bActivate);
else if((bActivate==TRUE && pActivateView==this) || (bActivate==FALSE && pDeactiveView==this))
pOIAObj->OnDocWindowActivate(bActivate);
pOIAObj->Release();
}
}
}


To pass a command to an ActiveX document, the
IOleCommandTarget is used. With help of the “ExecuteCommand” method
the view passes commands to it’s document.



void CAXDocContainerView::ExecuteCommand(DWORD nCmdID, DWORD nCmdExecOpt)
{
IOleCommandTarget* pCT=NULL;

IOleDocument* pDocument=NULL;
HRESULT hr=GetDocItem()->m_lpObject->QueryInterface(IID_IOleDocument, (void**)&pDocument);
if(SUCCEEDED(hr))
{
hr=pDocument->QueryInterface(IID_IOleCommandTarget,(void**)&pCT);
if(SUCCEEDED(hr))
{
hr=pCT->Exec(NULL,nCmdID,nCmdExecOpt,NULL,NULL);
pCT->Release();
}

pDocument->Release();
}
}

void CAXDocContainerView::OnFilePrintPreview()
{
// TODO: Add your command handler code here
ExecuteCommand(OLECMDID_PRINTPREVIEW, OLECMDEXECOPT_DODEFAULT);
}

void CAXDocContainerView::OnFilePrint()
{
// TODO: Add your command handler code here
ExecuteCommand(OLECMDID_PRINT, OLECMDEXECOPT_DODEFAULT);
}

void CAXDocContainerView::OnFilePrintSetup()
{
// TODO: Add your command handler code here
ExecuteCommand(OLECMDID_PAGESETUP, OLECMDEXECOPT_DODEFAULT);
}


 


From the other side, user can invoke commands by
pressing buttons on the toolbars created by server programs, and we should find
a way to catch these commands. For this reason, we must implement the
IOleCommandTarget interface for the inplace frame where our client item resides.
The inplace frame is encapsulated by the undocumented MFC class COleFrameHook.
The CAXDocContFrameHook class was derived from the COleFrameHook class to add
the OLE command target support. This class, when processing an OLE command,
creates an instance of the COleCmdUI class, fills it’s values properly and
calls it’s DoUpdate method. The COleCmdUI class makes it possible to map
an OLE command onto an MFC command, so it is possible to use something like the
following:



BEGIN_OLECMD_MAP(CAXDocContainerApp, CWinApp)
{NULL,OLECMDID_NEW,ID_FILE_NEW},
{NULL,OLECMDID_OPEN,ID_FILE_OPEN},
{NULL,OLECMDID_SAVE,ID_FILE_SAVE},
{NULL,OLECMDID_PAGESETUP,ID_FILE_PRINT_SETUP},
{NULL,OLECMDID_PRINT,ID_FILE_PRINT},
{NULL,OLECMDID_PRINTPREVIEW,ID_FILE_PRINT_PREVIEW},
END_OLECMD_MAP()

It was not easy to attach the CAXDocContFrameHook class
to the OLE client item. To do that I had to re-implement the IOleInPlaceSite
interface for the derived class. I just copied the code for COleClientItem but
replaced the COleFrameHook creation with the creation of my class.

Having implemented all these steps I got a program that
is able to behave like the Visual Studio (ActiveX document support). Then I
added just one more document – script document to run scripts. In this
article I will not explain the steps to create such a document. Read two
previous my articles for this. I just added the OleOpenDocumentFile function to
open documents from within the scripts, so that I would be able to drive
documents with help of the OLE automation.



LPDISPATCH CScriptDocument::OleOpenDocumentFile(LPCTSTR strFileName)
{
// TODO: Add your dispatch handler code here
CDocument* pDoc=AfxGetApp()->OpenDocumentFile(strFileName);
if(pDoc && pDoc->IsKindOf(RUNTIME_CLASS(CAXDocContainerDoc)))
{
CAXDocContainerDoc* pContDoc=(CAXDocContainerDoc*)pDoc;
CAXDocContainerCntrItem* pItem=pContDoc->GetDocItem();

if(pItem)
{
return pItem->GetIDispatch();
}
}

return NULL;
}


It is not evident how to obtain a dispatch interface to
the embedded item. For this reason the CAXDocContainerCntrItem::GetIDispatch()
method was created by copying the sample from the MFC technical note 39.

 


One more important thing. Never run scripts in the
SCRIPTSTATE_RUNNING state. You may have unpredicted results. It is better to
fire events or to call the main subroutine. Here goes a code how you can run the
main() subroutine from within a c++ code.



//Running the main subroutine
IDispatch* pScriptDispatch=m_ScriptEngine.GetScriptDispatch(pstrItemName);
if(pScriptDispatch)
{
DISPID dispid=NULL;
OLECHAR* szMainFunc = T2OLE(_T(“main”));

HRESULT hresult = pScriptDispatch->GetIDsOfNames(IID_NULL,
&szMainFunc ,
1,
LOCALE_SYSTEM_DEFAULT,
&dispid);

if(SUCCEEDED(hresult))
{
//The driver will release the pScriptDispatch in any case
COleDispatchDriver driver(pScriptDispatch);
driver.InvokeHelper(dispid, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
}
else
pScriptDispatch->Release();
}
}


 


If you have any question about ActiveX script hosting or
other questions about COM, contact me by e-mail:[email protected]

If you have anything to share with me, use the same
address.

Regards,


Andrew

Download Project Files for VC++ 5.0 – 101 KB

Note: (By V. Rama Krishna)

I faced some problems in compiling and Building with VC++ 6.0. After you extract the above project
files, please extract the following zip file and overwrite files in the original directory

Additional Download for VC++ 6.0 Users- 13 KB

More by Author

Must Read