Using .NET to access MathType's OLE subsystem

Please note this document applies only to MathType 6.9 and later, for Windows. Mac users should continue using the MathType 5 SDK.

The purpose of this document is to outline how the .NET Microsoft Word Add-In interacts with MathType's OLE server (MathType 6.5 and greater). After reading this document the developer should have a better understanding of how to leverage MathType's IDataObject implementation through .NET, how to connect to a MathType server, and how to use IDataObject instances with MathType.

This sample Microsoft Word Add-In uses the COM interoperability layer in C#. It is quite easy to gain access to the well known COM interfaces. Well known COM interfaces for the purpose of this interoperability layer are defined as the following: interfaces that are published by Microsoft or 3rd party vendors that Microsoft licenses interfaces. Examples of well known interfaces which will be used in this example are IDataObject and IOleObject.

In order to start, edit, and show the results of operations utilizing an IDataObject, there are a few operations that first must be done.

First the currently active Word document must be retrieved. This is quite simply done with the following line:

// Get the active document. 

object doc = _applicationObject.GetType().InvokeMember("ActiveDocument", BindingFlags.GetProperty, null, _applicationObject, null);

Once the active document is retrieved it can then be invoked, as seen below, to verify if there is a shape collection.   This collection may contain one or more OLE objects that maybe of interest to MathType.

// Retrieve the shapes collection from the current word document.

Word.InlineShapes shapes = (Word.InlineShapes)doc.GetType().InvokeMember("InlineShapes", BindingFlags.GetProperty, null, doc, null);

This will allow the developer to get the current set of inline shapes by using the InvokeMember on the document. These shapes must then be iterated over to determine if the current iterated shape is an OLE aware object. If the shape is an OLE aware object, the OLEFormat property on the InlineShape will be non-null. If the OLEFormat is non-null, there will be a property contained within the OLEFormat, its program id (OLEFormat.ProgID), associated with the shape (of type InlineShape) object. This program id is used to find the class identification for the associated program. The program id for MathType is "Equation.DSMT4". If the program id is "Equation.DSMT4", MathType can edit this shape object. The next step is to determine how to find MathType from this program id.

Once the determination has been made that the InlineShape is of interest to MathType, the process of finding the correct application for editing the shape begins. In order to find the correct class id for the associated program id, the application cannot just take the CLSID for the program ID. The application associated with the shape may have been upgraded, where the previous version of the application no longer exists on the system. This program id may eventually resolve to a completely different class id, resulting in an application that is different than previously thought. In order to resolve to the correct CLSID a chain of auto-convert CLSIDs must be followed. To find the end result CLSID the user must recursively search the auto-convert chain. In the sample code the function FindAutoConvert is called. In FindAutoConvert the program id is first converted to its corresponding CLSID (through an OLE32 call to CLSIDFromProgID). If the conversion from program id to CLSID does not return successfully then the application that would have consumed the shape is either not installed, or at least not installed correctly. However if the conversion is successful, the process of recursively iterating through the auto-convert chain begins. 

To recursively iterate through the auto-convert chain it is really quite simple. For each CLSID that is received from OleGetAutoConvert a couple of checks must be completed. First, the simplest of these, is checking if the CLSID returned from OleGetAutoConvert is equal to the CLSID that is being requested for the conversion. If the two CLSIDs are equal then the end of the chain has been found. If the two CLSIDs are not equal, then the returned CLSID should be passed as a parameter to the next call of the recursive function, see RecurseAutoConvert in the sample code. Once the end of the auto-convert chain has been reached, the recursive calls will return a CLSID that represents the application that can edit the shape. This CLSID is returned as an out parameter (autoConvert).

With the CLSID in hand from the previous step, now a check needs to be done to determine if the application physically resides on the system. To find where the application resides on disk use the CLSID from the auto-convert step, and pass that CLSID to the function DoesServerExist. This function will open the registry and find the subkey where the LocalServer32 resides and conducts a file system check for the application. This function will return true if the application referenced by the CLSID is actually present on the system, else it will return false if the application is not present on the system.

In determining if the application supports the requested format, there is no need to start the application; this would needlessly consume system resources, when it is simply not required. Note, when a server is registered for OLE automation (at install) it will also register the formats that the server can consume. In the sample code, the Add-In concerns itself with the ability to handle the MathML formats; all other data types for the purposes of this example are ignored. In the sample code DoesServerSupportFormat confirms whether the requested data type is supported. This function opens the registry using the CLSID from the auto-convert process and retrieves the DataFormats\GetSet subkey. This subkey contains the data formats that are supported by the application referenced by the program id. The method will further iterate through all of the values in the subkey which are contained as strings. These strings are compared against the requested format, in this case MathML. If the data format is found then it will return true, else false.

In order to start the required application correctly, the program using OLE automation must use the correct verb in order to get the desired server behavior and startup conditions for the server application. From OLE there are some well known verbs, for example open, and edit. But OLE server applications can define their own custom verbs as well; this is the focus of this section.

The example code shows the use of a special application verb. This verb is known by the application only and not by other applications. For the purposes of this document the verb string "RunForConversion" will be the custom verb of interest. One thing to note about custom verbs, they are positive values; OLE defined verbs are negative integer values, see MSDN for more information on OLE verbs. In order to find the integer for the verb name, again the CLSID from the auto-convert must be used. In GetVerbIndex this function will utilize two different parameters. The first parameter is the string name of the verb, the second parameter is the CLSID derived from auto-convert search. Again the registry is used to determine which verbs are available; this search is conducted by opening the Verb subkey. The GetVerbIndex function will iterate through all of the sub keys until it comes to an end of the verb list or the custom verb is found. For each subkey retrieved its value is searched for the verb string of interest. In our example once the search loads the subkey containing "&RunForConversion,0,2" and a string find is conducted, our verb is found at that point. Now that the verb string has been found and the index for the verb is returned, this index corresponds to the subkey loaded from the registry. This value is passed to DoVerb in order to start the application. If the verb index value returned is 999 then the verb index was not found.

The Add-In now has to determine whether the IDataObject can actually support MathML in any of its three supported variants, MathML, MathML Presentation, or application/mathml+xml. Earlier, the sample queried the registry to verify if MathML is a supported data format for the application. However it does not know if the OLE Object supports the MathML format in any form, the two maybe are out of sync. The best mechanism to check if an OLE Object contains MathML is to query the object for the MathML data format. In the sample code there is a function by the name of CanUtilizeMathML which conducts this check, here is how it works. 

The server must have been started using the DoVerb function which is part of OLEFormat. This will give the OLE object a connection to the application server. Once the connection is verified, the IDataObject is retrieved. How this is accomplished is discussed in the section entitled Starting MathType Server. Once the IDataObject is retrieved from the OLEFormat.Object, this data object becomes the input parameter to the CanUtilizeMathML function. The return value of the CanUtilizeMathML is an output parameter that will return the variant of MathML that is supported by the application. This format can be of type MathML, MathML Presentation, or application/mathml+xml. However, in order to determine which MathML variant can be supported, the IDataObject must be queried. To make this query the FORMATETC structure must be created and filled out. The structure itself is really quite straight forward, the next short code example shows how to fill out a FORMATETC structure.

// Initialize a FORMATETC structure to get the requested data
formatEtc.cfFormat = (Int16)dataFormatMathML.Id;
formatEtc.dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT;
formatEtc.lindex = -1;
formatEtc.ptd = (IntPtr)0;
formatEtc.tymed = TYMED.TYMED_HGLOBAL;

There is one piece that will be different for each query. That is the cfFormat member of the structure. For each different query this member must change to reflect the type that is to be queried. To get each of the different types they must be queried for from the clipboard sub-system. This is accomplished by the following section of code:

// Find within the clipboard system the registered clipboard format for MathML
// This data format would have been registered with MathType.
dataFormatMathML = DataFormats.GetFormat("MathML");
dataFormatMathMLPres = DataFormats.GetFormat("MathML Presentation");
dataFormatAppMathMLXML = DataFormats.GetFormat("application/mathml+xml");

The DataFormats object queries the clipboard sub-system for particular format types. In C/C++ there is a Win32 call, RegisterClipboardFormat, which accomplishes the same thing. It will take the string name and return a unique registration number for the clipboard format, this value is registered within the operating system and is global to that user's Operating System. If the format is already registered, as in the case of MathType after the server has been started, the DataFormats.GetFormat will return the number registered to that particular custom clipboard format. This format will have the same value as the registration that is contained by MathType. This value is then used to populate the cfFormat value in the FORMATETC structure. Once the FORMATETC structure is filled out, the query of the IDataObject may commence.

The sample code will first search for the requested MathML format. In this function there are a total of three different variants of MathML formats searched: MathML, MathML Presentation, application/mathml+xml. Here is the code that will query for the different MathML variations.

// Check all of the MathML formats, MathML, MathML Presentation, application/mathml+xml.
while(x < countFormats) {

    // FORMATETC.cfFormat as defined in the API is a clipFormat, this is define by the CLR as 
    <code>// an Int16.
    formatEtc.cfFormat = (Int16)finder[x].format.Id;

    // Return value for the data QueryGetData check.
    int queryReturn = -1;
    if (dataObject != null) {

        // Query for the MathML type data format
        queryReturn = dataObject.QueryGetData(ref formatEtc);

        if (queryReturn == 0) {

            // Check to see if the requested format was just queried.
            // If so leave the searching and return back to the caller.
            if (finder[x].format.Name == dataFormatRequested) {
                dataFormat = finder[x].format;
                return true;
            }
            else
            {
                // Since we have a positive QueryGetData then there is at least one
                // MathML format supported.
                canProvideMathML = true;
                finder[x].verified = true;
            }
        }
    }
    x++;
}

As a preface to the search above, a simple array (finder) was populated with the three MathML clipboard format variants. The Finder array is nothing more than a simple class object that contains the DataFormats.Format member containing the registered clipboard format, and a Boolean stating if the format is verified supported. The IDataObject QueryGetData is used to determine if the OLE Object supports that particular format passed from the finder object. This call to QueryGetData is done for each of the items in the finder array. It will break out of this loop if it finds that the requested format is found. If the requested format is not found then it will continue to iterate through all of the formats to verify which of the custom clipboard formats are supported.

Once the format verification process is completed, this function must select one of the MathML formats to be used. This is simply an iteration to get the first verified supported MathML format. That format is returned on the out parameter labeled dataFormat.

This section applies to both Equation_GetMathML and Equation_SetMathML. In the course of any OLE automation with the shape object there are a number of tasks that must be accomplished. All of these tasks have been outlined in the previous sections. With these tasks now complete, it is time to start the MathType server and edit the shape.

First the server must be started. This is done by using the shape and its OLEFormat property. On the property there is a function called DoVerb. When this is called with the appropriate verb index it will start the server in the appropriate mode, based on the support within the server application. Once the server is operational, a check for the actual data object is done. Again this shape contains a data object in the OLEFormat property, on its Object property. Once the object is retrieved, it is time to get references to the following two interfaces: IDataObject, and IOleObject. C# with its COM interoperability has made it very simple to get interfaces from its objects, as long as the interfaces are supported. Here is an example of how to get object references to the requested interfaces in C#:

oleDataObject = dataObject as IDataObject;
oleObject = dataObject as IOleObject;

The retrieval of the interfaces is very straight forward. It is simply using the "as" keyword. Both interfaces are abbreviations for the following:

usingIDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
usingIOleObject = Microsoft.VisualStudio.OLE.Interop.IOleObject;

If both the oleDataObject and oleObject are successfully queried, then the normal OLE GetData and SetData setup with FORMATETC and STGMEDIUM structures may begin. With C# there are some difference with the C/C++ usage of the aforementioned structures. Just reference the sample code to see an example of these structures how these structures are filled out. 

Earlier there was a search to verify that MathType supported MathML or one of its variants. This data format must be retrieved from the clipboard sub-system. Earlier there was a call to CanUtilizeMathML, this function call returned the data format that will be used for the GetData or SetData calls. This returned value must be assigned to the FORMATETC.cfFormat member.  

When a request to IDataObject::GetData is done, both the FORMATETC and STGMEDIUM structures must be filled out. Just like the C/C++ versions of STGMEDIUM the tymed value must be set to TYMED_NULL. This shows that no data is being passed in the call to GetData. If that member is anything other than TYMED_NULL, an error will result with the call to GetData. In the sample code, GetData is requesting "MathML" or one of its variants from the shape object via the MathType server. When the call to GetData returns there will be HGLOBAL data representing "MathML" or one of its variants in the STGMEDIUM's unionmember, and STGMEDIUM's tymed will be filled out to be TYMED_HGLOBAL. In the sample code a simple call to WriteOutEquationFromStgMedium, will display the "MathML" markup in a message box. 

To properly release the IDataObject and its resources, there must be a call to close the IOleObject. The IOleObject interface is part of the same data object. The IOleObject interface is how the data object will be closed and resources released. This step must be done, or there may not be enough resources available to iterate over all of the shapes. This is a very simple call that is done at the end of Equation_GetData. 

if (oleObject != null)
    oleObject.Close((uint)Microsoft.VisualStudio.OLE.Interop.OLECLOSE.OLECLOSE_NOSAVE);

When a request to IDataObject::SetData is done both FORMATETC and STGMEDIUM structures are filled out as they were in GetData. However with SetData the tymed and unionmember fields of STGMEDIUM must be filled out prior to being sent to SetData. In order to get the memory marshalling correct for the data, it must be allocated and set using the C# marshal type. Marshalling the data will properly allocate and assign the HGLOBAL type memory to the STGMEDIUM unionmember. In the sample a hard coded simple MathML string is being set into the STGMEDIUM. Also the STGMEDIUM tymed will be set with the value TYMED_HGLOBAL. Remember the tymed and data type set into the unionmember must match. If they do not match, an error will occur from within SetData. When SetData is called the MathType server will extract the "MathML" from the union member, and apply this data to the equation data contained in the shape. Once this is completed the server will tell the client (Microsoft Word) to have the shape redraw itself in the Word document, and the SetData call will return.

To properly release the IDataObject there must be a call to close the IOleObject. The IOleObject interface is part of the same data object. The IOleObject interface is how the data object will be closed and resources released. This step must be done, or there may not be enough resources available to iterate over all of the shapes. This is a very simple call that is done at the end of Equation_SetData. 

if (oleObject != null)
    oleObject.Close((uint)Microsoft.VisualStudio.OLE.Interop.OLECLOSE.OLECLOSE_NOSAVE);

As stated earlier the purpose of this document is to show how a .NET application may access MathType without the SDK, using OLE. As can be seen from the sample code it is really quite straight forward to iterate over the shapes in the document and conduct GetData and SetData operations with the OLE supported shape objects.