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< CPtrArray,CAXDocInfo*>
{
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< 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:andrew@skif.net.

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