MindManager 7 ToDoList AddIn development Part 6: the Mysteries of COM interop

Sometimes there are inevitable mysteries uncovered when writing code.  Well, I’ve bumped into quite a rich source of mysteries in trying to use an aspect of the MindManager object model that thunks through a brittle COM interop module, known as CmjDocumentCollectionComObject.  Here’s just a couple of examples that have come up recently:

“unable to create document”

Here is the ‘offending’ code:

public static MMInterop.Document GetMap(string filename)
{
    MMInterop.Document toDoListMap;
    MMInterop.Documents maps;
    string toDoListMapFullPath = Connect.applicationObject.get_Path(MMInterop.MmDirectory.mmDirectoryMyMaps) + filename;
    maps = Connect.applicationObject.get_Documents(true);
    try // open a ToDoList map
    {
        toDoListMap = maps.Open(toDoListMapFullPath, String.Empty, true); // here's where the COMException is raised
    }
}

Here is the exception raised:

System.Runtime.InteropServices.COMException occurred
  Message=”Object ‘CmjDocumentCollectionComObject’ reports an error: ‘unable to create document'”
  Source=”MindManager.Application.7″
  ErrorCode=-2147220992
  StackTrace:
       at Mindjet.MindManager.Interop.DocumentsClass.Open(String pFileName, String pPassword, Boolean Visible)
       at ParanoidMike.MindManager.ToDoList.ToDoListMap.GetMap(String filename) in C:\personal\VS Projects\MM7TODOList\ToDoList.cs:line 187
  InnerException:

And do you want to know what that exception really means?  “The specified file does not exist” would be my interpretation.  If I understand the MindManager function maps.Open() correctly, this is meant to open an existing document.  I’ve just asked it to open a non-existent document, so I’d expect “can’t find it”, but I’m puzzled by “unable to create document”.  It’s like whoever wrote the error strings for the CmjDocumentCollectionComObject is interpreting the name of the Win32 CreateFile() API literally.

I am attempting to use a Try…Catch approach to testing whether the requested file exists. If the file exists, then maps.Open() would succeed; if it didn’t exist, then I’d use the Catch block to instead create the named file.  I didn’t expect to have to catch a System.Runtime.InteropServices.COMException, nor in figuring out how to catch only the exception with a specific ErrorCode/HRESULT returned by the COM object.

But then again, “Nobody expects the Spanish Inquisition”

{“Retrieving the COM class factory for component with CLSID {5B9EA9CE-76A3-4878-9A6B-22D0A3042774} failed due to the following error: 80040154.”}

Here’s the code:

MMInterop.Document toDoListMap;
try
{
    toDoListMap = new MMInterop.Document();
}
catch (System.Runtime.InteropServices.COMException e)
{
    throw;
}

Here’s the .NET exception that gets thrown:

{“Retrieving the COM class factory for component with CLSID {5B9EA9CE-76A3-4878-9A6B-22D0A3042774} failed due to the following error: 80040154.”}

And here’s the HRESULT that is being thrown by the COM object: -2147220992

In various posts, the common theme seems to be that the “component with the noted CLSID” needs to be re-registered.  Searching for this CLSID on my system (predictably) leads to Mindjet.MindManager.Interop, Version=7.0.323.0, Culture=neutral, PublicKeyToken=19247b5ea06b230f and Mindjet.MindManager.Interop, Version=7.1.388.0, Culture=neutral, PublicKeyToken=19247b5ea06b230f.  [However, there’s no ProgID registered for Mindjet.MindManager.Interop.  Is that bad — I know it’s not good for typical apps, but is a COM interop assembly a “typical” COM app?]

I remember reading somewhere that the MindManager 7 Primary Interop Assemblies were installed by default when installing MM7, so I thought perhaps and Add/Remove Programs “Repair” operation would suffice.

Unfortunately no — damned MindManager, I ran the full Repair, and it even reinstalled all the 41 default templates (so I know it did completely successfully), but my code is still throwing this same error.  So I went looking for the file path, which meant examining the Properties of the “MindManager” Reference, and it reports that this “ActiveX” file is stored here: C:\WINDOWS\assembly\GAC_MSIL\Mindjet.MindManager.Interop\7.1.388.0__19247b5ea06b230f\Mindjet.MindManager.Interop.dll.  Fire up a Command Prompt in that directory, run “REGSVR32.EXE Mindjet.MindManager.Interop.dll”, and I end up with this error:

image

REGASM.EXE

Here’s a riddle: how does one initialize a variable that cannot be initialized?

When I try to create a new mindmap document using a .NET AddIn, I’m finding that the variable I declare to hold a reference to the new mindmap is not usable: (a) it won’t work until it’s initialized, but (b) when it’s initialized it throws the error documented here:
http://tech.groups.yahoo.com/group/MindManagerDev/message/447

I’ve tried to dig up any hints that would help me figure out what to do to resolve this error, but so far nothing has worked (including trying to Register the Interop DLL, and Repairing the installation of MindManager).

There are plenty of people asking about this kind of issue — some related to straight COM objects, others talking about COM Interop assemblies:
http://www.thescripts.com/forum/thread731361.html
http://forums.cnet.com/5208-6141_102-0.html?forumID=8&threadID=216925&messageID=2313201
http://channel9.msdn.com/ShowPost.aspx?PostID=333247
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=765439&SiteID=1
http://groups.google.com/group/microsoft.public.dotnet.framework.interop/browse_thread/thread/e8d8ccb58221f843/2263fa2b3db5895b

(1) I tried loading up the Interop DLL in DEPENDS.EXE, but there appear to be no missing dependencies.
(2) I tried registering the assembly using REGASM.EXE, according to the article here: http://www.simple-talk.com/dotnet/visual-studio/build-and-deploy-a-.net-com-assembly/
using the following command:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727>regasm C:\WINDOWS\assembly\GAC_MSIL\Mindjet.MindManager.Interop\7.1.388.0__19247b5ea06b230f\mindjet.mindmanager.interop.dll
Microsoft (R) .NET Framework Assembly Registration Utility 2.0.50727.1433
Copyright (C) Microsoft Corporation 1998-2004.  All rights reserved.

Types registered successfully

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727>

Now I’m getting the same .NET exception, with a different HRESULT: -2147221164.  That HRESULT appears to be associated with needing to re-register Atl.dll, so I gave that a shot.  Unfortunately, it gave me the same error and the same HRESULT.  I also tried re-running the above REGASM.EXE command, with no change — still throwing the same exception.

Cheap Hack: avoid init issues with dummy calls

I’m almost embarrassed to even discuss how I’m getting around this intractable problem…but not embarrassed enough not to do it, so I might as well own up to it.  Who knows?  Maybe one of you will know what I’m overlooking.  Maybe I’ll find this article in six months’ time and remember why I put this hack in place.  Maybe this will just be my own personal legacy to the world. 🙂

Let’s recap: my design for this AddIn is to use a separate map to list copies of all Tasks found in the searched maps. If the named map already existed, just open it; if not, a new map should be created.  However, I kept getting blocked by not “initializing” the local variable to hold the new/opened Document handle before returning it to the calling function.  However, initializing the local variable with a “new MMInterop.Document()” call just caused other problems.

I was thinking that because Visual Studio was throwing the CS0165 error “Use of unassigned local variable ‘toDoListMap” on the final line of code “return toDoListMap”, and not on any of the preceding where the toDoListMap variable was being assigned, what Visual Studio might be reacting to is that toDoListMap wouldn’t have a non-null value on all code paths through the function.  I’d ignored this thought because (a) the code paths where it wouldn’t get a value were intentional, and (b) the documentation on CS0165 kept indicating that I needed to initialize the variable.

However, after exhausting all the “right way” options, I finally just dropped some initializations (assignments?) into the code paths where toDoListMap wasn’t previously being touched:

public static MMInterop.Document GetMap(string filename)
{
    MMInterop.Document toDoListMap;
    MMInterop.Documents maps;
    string toDoListMapFullPath = Connect.applicationObject.get_Path(MMInterop.MmDirectory.mmDirectoryMyMaps) + filename; // TODO: remove this hard-coded pathing to the ToDoList map
    maps = Connect.applicationObject.get_Documents(true); // assign/initialize the maps variable so the array can be searched by maps.Open().  "Connect" is the Class that implements a static variable "applicationObject" which is assigned to the "application" object that's captured during AddIn initialization.

    try // open the existing ToDoList map
    {
        toDoListMap = maps.Open(toDoListMapFullPath, String.Empty, false);
    }
    catch (System.Runtime.InteropServices.COMException e) // if a ToDoList map by that name does not exist, create one
    {
        if (e.ErrorCode == -2147220992) // "Object 'CmjDocumentCollectionComObject' reports an error: 'unable to create document'"
        {
            System.Windows.Forms.MessageBox.Show("Exception opening map: " + e.Message); // TODO: remove once initial debugging is complete
            string templateFullPath = Connect.applicationObject.get_Path(MMInterop.MmDirectory.mmDirectoryTemplates) + templateFileName; //construct reference to the template that's required when creating a new Document

            toDoListMap = maps.AddFromTemplate(templateFullPath, String.Empty, false);
        }
        else // this is never intended to be run - it's just here to convince the compiler that the toDoListMap variable has been initialized on all code paths, as it otherwise throws an exception "Use of unassigned local variable 'toDoListMap'.
        {
            System.Windows.Forms.MessageBox.Show("If you see this message, record the error and report it to the developer - this should never be seen: error code = " + e.ErrorCode.ToString());
            toDoListMap = new MMInterop.Document();
        }
    }
    catch (Exception e) // this is never intended to be run - it's just here to convince the compiler that the toDoListMap variable has been initialized on all code paths, as it otherwise throws an exception "Use of unassigned local variable 'toDoListMap'.
    {
        System.Windows.Forms.MessageBox.Show("If you see this message, record the error and report it to the developer - this should never be seen: error code = " + e.ErrorCode.ToString());
        toDoListMap = new MMInterop.Document();
    }

    return toDoListMap;
}

And now, miraculously this damned code compiles and runs correctly.  I really wish I had some better ideas than these “new” calls, ’cause I’ve got a feeling they’ll come back to bite me far down the line.  I’ve added some MessageBox “please let me know if you see this” dialogs, but that won’t really solve the problem — just make me look a little more like an amateur code-jockey.

[I’m still not sure why in this case, I was able to assign the toDoListMap variable without first initializing it — it’s not one of the simple datatypes, so I don’t think it’s being implicitly initialized by the compiler, and I thought I’d just re-learned that a complex Object always needs to be declared, initialized and then assigned.  Once again there’s something I don’t get about this, but I’ll leave that to dig up in the future, as yet another great surprise. :)]

Advertisements

4 thoughts on “MindManager 7 ToDoList AddIn development Part 6: the Mysteries of COM interop

  1. Mike – Below is a simplified form of your GetMap() method, probably garbled by the comment editor.The statementMMInterop.Document doc = new MMInterop.Document();will always fail. The MindManager object model uses Add() method on collections to create new members for that collection. Under the covers, the .Net interop classes translate the C# “new” keyword into a COM idiom, something like CoCreateInstanceEx(). But the MindManager Interop layer doesn’t implement the support to allow Document instances to be created out of the blue. Like most OLE Automation object models, the only class creatable from by an outside client is the MindManager Application. But this “CreateObject” behavior usually not needed. For MindManager macros, the Application object is always part of the context. For Add-ins, MindManager passes an obkect implementing the MMInterop.IAplication interface in the IDTExtensibility2.OnConnection() call used to load the Add-in. Finally, IMHO it’s generally a bad idea to use catch clauses for “normal” processing, like creating a new ToDoList map document. But that’s sort of a style thing.Hope the following Class1 sample helps. I didn’t compile/test it, but it should be pretty close.Regards,dethomasusing MMInterop = Mindjet.MindManager.Interop;using System;using System.IO;public class Class1{ /// Return the ToToList map, creating a blank map if needed public static MMInterop.Document GetMap(string filename) { MMInterop.Document toDoListMap; // assign/initialize a maps collection so it can be searched by maps.Open(). // “Connect” is the Class that implements a static variable “applicationObject” which is // initialized to the “application” object captured during AddIn initialization. MMInterop.Documents maps = Connect.applicationObject.get_Documents(true); // TODO: remove this hard-coded path to the ToDoList map, maybe get it from an Options cache // if filename does not exist. string toDoListMapFullPath = Connect.applicationObject.get_Path(MMInterop.MmDirectory.mmDirectoryMyMaps) + filename; try // open the existing ToDoList map { if (File.Exists(toDoListMapFullPath)) { toDoListMap = maps.Open(toDoListMapFullPath, String.Empty, false); } else { toDoListMap = maps.Add(false); // // could use maps.AddFromTemplate(templateFullPath, String.Empty, false); // assuming templateFullPath addresses a valid MindManager template file toDoListMap.FullName = toDoListMapFullPath; toDoListMap.Save(); } } catch(System.Exception e) { MessageBox.Show(“GetMap() failed:” + e.Message); throw e; } return toDoListMap; } // public static MMInterop.Document GetMap(string filename)} // public class Class1

    Like

  2. I tried your solution on MindManager 8 and it works. An alternative is to just put the map in the “My Maps” directory and MMInterop.Documents.Open()works okay.

    Like

  3. Is it possible download a ready compiled version of this .dll somewhere. I don’t have Visual Studio compile it by myself

    Like

  4. Nice post! Provides some good info for people evaluating different approaches. In the proxy part, you might want to mention that it does introduce a little overhead. It's usually small compared to the benefit that you get from it, but still significant.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s