Mashing the marvelous wrapper until it responds, part 1: prereq/setup

I haven’t used a dynamic language for coding nearly as much as strongly-typed, compiled languages so approaching Python was a little nervous-making for me.  It’s not every day you look into the abyss of your own technical inadequacies and find a way to keep going.

Here’s how embarrassing it got for me: I knew enough to clone the code to my computer and to copy the example code into a .py file, but beyond that it felt like I was doing the same thing I always do when learning a new language: trying to guess at the basics of using the language that everyone who’s writing about it already knows and has long since forgotten, they’re so obvious.  Obvious to everyone but the neophyte.

Second, is that I don’t respond well to the canonical means of learning a language (at least according to all the “Learn [language_x] from scratch” books I’ve picked up over the years), which is

  • Chapter 1: History, Philosophy and Holy Wars of the Language
  • Chapter 2: Installing The Author’s Favourite IDE
  • Chapter 3: Everything You Don’t Have a Use For in Data Types
  • Chapter 4: Advanced Usage of Variables, Consts and Polymorphism
  • Chapter 5: Hello World
  • Chapter 6: Why Hello World Is a Terrible Lesson
  • Chapter 7: Author’s Favourite Language Tricks

… etc.

I tend to learn best by attacking a specific, relevant problem hands-on – having a real problem I felt motivated to attack is how these projects came to be (EFSCertUpdater, CacheMyWork).  So for now, despite a near-complete lack of context or mentors, I decided to dive into the code and start monkeying with it.

Riches of Embarrassment

I quickly found a number of “learning opportunities” – I didn’t know how to:

  1. Run the example script (hint: install the python package for your OS, make sure the python binary is in your current shell’s path, and don’t use the Windows Git Bash shell as there’s some weird bug currently at work)
  2. Install the dependencies (hint: run “pip install xxxx”, where “xxxx” is whatever shows up at the end of an error message like this:
    C:\Users\Mike\code\marvelous>python example.py 
    Traceback (most recent call last):     
        File "example.py", line 5, in <module>
            from config import public_key, private_key 
    ImportError: No module named config

    In this example, I ran “pip install config” to resolve this error.

  3. Set the public & private keys (hint: there was some mention of setting environment variables, but it turns out that for this example script I had to paste them into a file named “config” – no, for python the file needs to be named “config.py even though it’s text not a script you would run on its own – and make sure the config.py file is stored in the same folder as the script you’re running.  Its contents should look similar to these (no, these aren’t really my keys):
        public_key = 81c4290c6c8bcf234abd85970837c97 
        private_key = c11d3f61b57a60997234abdbaf65598e5b96

    Nope, don’t forget – when you declare a variable in most languages, and the variable is not a numeric value, you have to wrap the variable’s value in some type of quotation marks.  [Y’see, this is one of the things that bugs me about languages that don’t enforce strong typing – without it, it’s easy for casual users to forget how strings have to be handled]:

        public_key = '81c4290c6c8bcf234abd85970837c97' 
        private_key = 'c11d3f61b57a60997234abdbaf65598e5b96'
  4. Properly call into other Classes in your code – I started to notice in Robert’s Marvelous wrapper that his Python code would do things like this – the comic.py file defined
         class ComicSchema(Schema):

    …and the calling code would state

        import comic 
        … 
        schema = comic.ComicSchema()

    This was initially confusing to me, because I’m used to compiled languages like C# where you import the defined name of the Class, not the filename container in which the class is defined.  If this were C# code, the calling code would probably look more like this:

        using ComicSchema;
        … 
        _schema Schema = ComicSchema();

    (Yes, I’m sure I’ve borked the C# syntax somehow, but for sake of this sad explanation, I hope you get the idea where my brain started out.)

    I’m inferring that for a scripted/dynamic language like Python, the Python interpreter doesn’t have any preconceived notion of where to find the Classes – it has to be instructed to look at specific files first (import comic, which I’m guessing implies import comic.py), then further to inspect a specified file for the Class of interest (schema = comic.ComicSchema(), where comic. indicates the file to inspect for the ComicSchema() class).

Status: Learning

So far, I’m feeling (a) stupid that I have to admit these were not things with which I sprang from the womb, (b) grateful Python’s not *more* punishing, (c) smart-ish that fundamental debugging is something I’ve still retained and (d) good that I can pass along these lessons to other folks like me.

Status and failure of CacheMyWork development

Every month or so I get an email like the following:

Hi, Love your app CacheMyWork. Left some suggestions for improvement on your website a while ago. Any chance you’ll release an updated version with more functionality any time soon? Seems to my it has huge potential once it’s updated.

I get this kind of email – wondering when I’ll finish the app, asking about status, wondering why it seems incomplete or why I haven’t integrated [easy fix “X”] – for only one of the open source projects I ever released: http://CacheMyWork.codeplex.com.

Every time I get one of these, I feel like a jerk for not finishing (or continuing) what I started, and I realize that if I was just a better coder, I’d have a lot of happy people out there.

I could never get my head around the databinding that is necessary to connect the WPF front end i built for the “version 2” of my app to the well-formed, totally functional app-finding algorithm I built literally years ago.

I feel like I’m disappointing a lot of people by not getting this back underway, but I really don’t have a clue how to fix the damned thing. I think my failure to maintain/improve this project is one reason why I’m giving up on my dream to ever be a professional coder.

I swear something must be wrong with my brain – every time I try to re-learn databinding concepts for .NET they look like they’re pretty simple for the author, but when I try to apply the ideas to my code, it never seems to work. I’ve coded three different data classes, I’ve tried every combination of parameters in the binding (both the XAML and the code-behind) I could find, but at best I get code parameters in the UI – never anything that hints that the bound data is leaking through (even though I can clearly see the data in the data class when I set breakpoints in the debugger). It’s like I’m not “getting” something about how this is supposed to work – it reminds me of how I was a week or two behind in introductory calculus class, when my brain couldn’t visualize what it was we were manipulating with those damned equations. (I finally got the calculus, though I think by now I’d have to start all over again.)

What would you do in this situation? I’d really like to get this going again – at least make good on the unfinished “new” release, and give myself some closure on that chapter of my geek life.

Building an Outlook Add-in: customizing the Contact Card, other options?

[I’m working on an app for work – enable me to pull job title, team name, etc. from a proprietary internal database, since our Exchange Address Book doesn’t contain that info.  Just to set the context for what the h*** I’m up to.]

Did a broad search for “Outlook 2010 VSTO” to find some beginners’ guide to writing something that will run in Outlook 2010.  Lots of possible stuff to read, but this 20-minute video seemed like my best bet:

How Do I: Create a WPF Control for Use Within Outlook 2010

Yes the question should be asked: are you better off writing an Outlook 2010 add-in using WPF or a Windows Forms?  I haven’t yet found a definitive answer, and they may both be supported to some degree, but the majority of articles I stumbled across were using WPF not Windows Forms.  That *may* just be a tyrannical bias coming from Microsoft, where XAML is all the rage and you’re ‘not allowed’ to talk about legacy development there anymore.  Regardless, if I see WPF and Outlook 2010 mentioned more than a half-dozen times, that *has* to be supported pretty well, so I’ll go with that for now.

Contact Card

Next I need to know what kind of Outlook “object” I’m trying to attach to.  I am aiming for that new intermediate Contact panel that shows up when you double-click the email address of an email in your Inbox (this example borrowed from Microsoft Support):

Contact Card example

  1. It already has a tabbed interface, so adding another tab makes intuitive sense as a user.
  2. It’s the first interface you get to detailed info on a user, so it’s the fewest clicks to get to an extended UI I could add.
  3. It’s new in Outlook 2010, which (I hope) means that the Outlook Interop object model has implemented first-class .NET support for interacting with that object.  [As opposed to some of the legacy objects in Outlook for which there is pretty crappy support in the .NET Interop model.]

Searches for “Outlook 2010 new features” take me to pages like this, which refer to this new interface as “a new easy-to-access contact card”.  Digging into the official Outlook blog there’s a detailed article “Using the contact card to learn who someone is”, so I’m definitely on the right track.  Now if only this was the same nomenclature used in the Outlook object model (developer side of the world) – it’s rare that the underlying code and the marketing names ever get aligned, so I’m not surprised much anymore if I don’t get that fantasy fulfilled.  [Heh, and just to show how easy it could’ve been, I realize now that the Contact Card is available as a context menu item when you right-click the email address.]

So my next search – Outlook 2010 “contact card” “object model” – turned up an article called “Customizing the Context Menu of a Contact Card in Outlook 2010”.  Among other things, this article states that “The IMsoContactCard object is defined in the primary interop assembly, Office.dll, of the Microsoft Office object library, not in Outlook 2010.”  So this is apparently an Office-wide object, available to other Office apps – not just Outlook (though it’s entirely possible Outlook is the only app that bothered).

Which also happens to lead to a Code Gallery sample called “Outlook 2010: Visual How Tos: Code Samples” that includes the sample code.  Unfortuately the sample is adding a new entry to the context menu, not to the Contact Card itself (which is a mite confusing, as the object model doesn’t make a really clear distinction between the two).  HOwever, this gives me a great lead on what area of the object model to focus my attention on.  And worse come to worst, I can always start with a really crude hack of adding a context menu selection that just pulls up the internal directory data for the selected user (or as I’m seeing from the code sample, the “Recipient” – gotta get your nomenclature aligned when you dive into an Office object model exploration).

Next let’s see if anyone out there has been monkeying with this object or this part of the object model – searches on Stack Overflow turn up nothing, but MSDN Social Forums hits some info on:

And a search of MSDN Library for IMsoContactCard led to one non-reference article: Extending the User Interface in Outlook 2010.  According to this article, adding a new item to the context menu when you right-click an email sender or recipient is done using the ContextMenuContactCardRecipient context menu.

Given that these articles all seem to say that it’s impossible to extend the contact card itself, I find myself with two alternatives:

  1. Add a right-click menu option that pulls the internal directory info for the selected recipient.
  2. Add another tab to the ‘legacy’ Outlook Properties window (which was the default in Outlook 2007 & 2003 when you double-click on a user).

Context Menu vs. Properties window

Comparing Option (1) and (2), I come to these benefits & drawbacks:

  1. The programming for the Contact Card context menu was just added in 2010, which probably makes it behave more consistently and robustly than the COM-based crap that comes with legacy Outlook features.
  2. Adding a tab to the Properties window (form) would assist me more easily if I wanted to “crawl up the address book” (i.e. look for the same information on the managers of the recipient I’m exploring).  I find I *can* get to a context menu for the recipient’s manager, but it’s hellishly buried and I’d probably be one of three people who’d find it (or who’d bother taking this path).
  3. From my recollection, Office (and Outlook in particular) can be really picky about exactly how and when to dispose of objects – generally resulting (from <100% perfect code) in deadlocks, memory leaks or difficulties in shutting down the hosting app.  I would imagine the Interop Assemblies have a harder time communicating reliably with the legacy COM object model (e.g. Properties window) than with objects only recently introduced (e.g. Fluent UI).
  4. While the Office Interop Assemblies have been incredibly forgiving about providing backwards-compatibility to all the COM objects that have been accumulated over the decades, I have to believe that Fluent UI customizations have a better future in coming versions of Office than COM-based customizations.  This should be especially true of Outlook, since that team took a “wait and see” approach to the Fluent UI in the Outlook 2007 generation.  If they’re still on board, they’ve benefited from the delay *and* it is likely they’re more committed than if they’d gotten burned by jumping in early.
  5. If I’m reading this right, Office Communicator (probably 2007 R2 and later) implements support for the IMsoContactCard – so a Fluent UI approach might actually give us coverage in Outlook *and* Communicator.  I don’t know how useful that really would be, but it *sounds* cool.

Thinking as an end user, I’d find another tab on the Outlook Properties window more intuitive, but I’d also be extremely unforgiving if my Outlook user experience slowed down or destabilized.  I don’t like the Context Menu approach that I seem to be left with in customizing the Fluent UI, but I can be optimistic that a more integrated approach will become apparent as my research continues – and in the meantime I’ll have a Fluent UI-compatible set of code to build on.

Details: Fluent UI

All these articles I’m finding talk about these Contact Card customizations in terms of customizing the “Fluent UI”.  I’m not sure, but I had believed that this Fluent UI was primarily introduced as a wrapping layer of menu/ribbon ‘cleanup’ of the Office UI that was long overdue by Office 2007.  These references make it sound as if the Fluent UI is where all new UI improvements are “homed” in Outlook 2010.

As I dig a little further, there are some pretty clear indications this is true:

Customize the Office UI

In Office 2010, the Office Fluent UI is fully customizable. This includes the ribbon, the Quick Access Toolbar, and the built-in context menus. By using the flexible XML-based markup and callbacks, you can create context menus by updating Open XML Format files or by using add-ins that are created in Microsoft Visual Studio.

Customizing Context Menus in Office 2010

In Office 2010, you can customize built-in context menus just as you can the other components of the Ribbon UI. This XML-based context menu extensibility model is based on the familiar Ribbon extensibility model. This means that you can use the same XML markup and callbacks that you currently use to customize the Ribbon UI.

Encouraging, but not specifically helpful other than a lot of hand-waving and empty promises.  Having clear documentation on what the object model does is the critical piece, and all I’ve got here so far is a Context menu (which is hardly an intuitive UI approach).  However, if that’s what I’ve got then it’ll have to do.  Off to implement code based on Customizing the Context Menu of a Contact Card in Outlook 2010 and see how well that treats me.

What do you like? Your ratings in multiple places – do you want to sync them?

The modern “what do you like?” systems are driving me nuts lately.  Every time I turn around, there’s a website offering to help me socialize, and what better way to find new friends (and new recommendations for media to consume) than by accumulating a bunch of ratings for the media I’ve already consumed?

Friends (and hopefully, to some degree) passers-by can then peruse my virtual “media shelves”, see what I’ve rated and how, and either (a) get or (b) give recommendations for stuff that’s related to what they/I have already seen/read/heard and liked.

Web 1.0: Amazon Recommendations

I’ve got > 1000 ratings for graphic novels accumulated on Amazon.com – which was a great way for years for me to get recommendations on other books by the same authors, but even more importantly and gratifyingly – to get recommendations on other books by authors I hadn’t yet read.  (I’ve always assumed some sort of Bayesian analysis that results in “people who liked your 4- and 5-star books also bought/owned/rated these items”.)

[I’d further tried to accumulate lists of ratings for music (for CDs I’ve purchased), but Amazon’s interface for this wasn’t nearly as sophisticated or predictive for music as it seems to be for books, so I’ve never gotten quite the same gratifying experience for music on Amazon.com.]

Web 1.0: Netflix Recommendations

I’ve got > 1000 ratings for movies accumulated on Netflix.com – which was a great way for years for me to get recommendations on other movies I’d like that have many of the same ephemeral qualities I enjoyed in the movies I rated most highly.  (Netflix has had a highly-publicized contest – which recently wrapped up – to come up with new ways of improving user recommendations, which to me meant that they’d exhausted all the available research into Bayesian and other mathematical analysis of the huge aggregations of data on what people liked, didn’t like, watched and marked “not interested”.)

Long before I was a Netflix subscriber though, I was (and am) a diehard advocate for IMDB.com.  [Hell, I was one of the lunatic adopters in the early days back when you had to submit queries to the IMDB via email.  Yeah, imagine browsing your favourite actor’s movies [and forget about TV – that didn’t count] by submitting a cryptically-formed email message and waiting the minutes it took for their servers to generate a response.  Fred Flintstone-style browsing.]  I’ve occasionally submitted a rating for movie through that site too, though I haven’t gotten any real benefit from it (except the knowledge that I’m helping to build the geek-slanted ratings that are the killer data set available from IMDB).

Web 2.0: social + recommendations

In the past year or so, I’ve fallen deeply in love with Facebook, Twitter and all the most interesting integrations with these “social platforms”.  With these platforms have come brand-new applications that allow you to rate movies/TV/books/music/whatever and not only get some kind of recommendations back from “the system”, but also to get much more specific and immediate feedback from those of your friends (or even “friends”) who’ve also signed up to use the application.  They see what you’ve rated, then respond with comments/replies/their own ratings, and can make much more specific (and personal, though statistically less predictive) suggestions of other stuff they want you to see/read/hear.

I love these – and while I’ve experimented with a bunch of these apps, I’ve gravitated to those apps that appear to have the greatest critical mass.  Not so much because I want a horde of strangers to help me find stuff, but because I’d like to reconnect with as many friends as possible and I hope they’re also at the apps I’ve picked.

So I’ve got ratings slowly accumulating at Goodreads.com, Flixster and a couple of others.  Goodreads has a great mobile site that makes it dead-easy to post a rating “on the go” with very few excess clicks, and the Flixster iPhone app is awesome *and* easy.  And there are dozens of other great sites where lots and lots of people are accumulating lots and lots of ratings data.

Problems: stale data, incomplete data, spread-too-thin efforts

After a while, I’ve noticed I’m spread thin across multiple places where these ratings are being accumulated.  It’s an unfortunate consequence of the abundance of such great sites and platforms, that I’m finding it hard to keep my ratings “in sync” between multiple places at once.  I have good intentions – and occasionally I’ll even follow through on those good intentions. 🙂

For example, I’ve got a ton of movie ratings in Netflix, but my primary interest for “sharing” movie ratings is moving to the Flixster app – mostly because it gives me a chance to get immediate feedback from a larger group of friends who catalogue their ratings and mini-reviews there via either Facebook or the iPhone app.  However, while I’m getting immediate gratification for my posts to Flixster, it’s not doing me any good in terms of system-generated (Bayesian) recommendations for other movies I want to watch. And when I go to my Netflix queue to add movies, I sometimes forget whether I’ve seen something (since I haven’t always rated those movies I’ve seen recently).

Similarly, the primary place I currently capture my ratings for graphic novels is in GoodReads (usually via the mobile-optimized web site that I access from my iPhone).  I’m not even getting any instant feedback from friends there, nor have I found any way to use the “crowd” of GoodReads users as a source for new recommendations.  However, there’s no alternative in my Web 1.0 world: Amazon doesn’t seem to have any way to add ratings on the go.  If you’re not going through their full browser, then you’re SOL.  (The Amazon iPhone app doesn’t do squat here, and neither does the mobile browser version – it’s almost as if they don’t care whether their customers like what they bought.)

I’m now split between worlds, and I suspect the world of Facebook/social media and other Web 3.0 apps will only make this worse – there’ll be more and more sites that all want you to provide some “sticky” information, that lures in more users ‘cause there’s a “crowd” there, and yet those ratings won’t be re-usable elsewhere.

This Ratings Data Ain’t Portable, My Friends

Yeah.  Twitter might have finally gotten religion that you “own” your Tweets, and Facebook *looks* like they’re convinced that you can and should have the ability to control your personal information/updates, but good luck trying to convince the thousands of little start-up apps out there, all hoping to lure you into their little walled garden and *keep* you there.

I expect that in 5-10 years, all these systems will be able to freely consume and re-use this data – the business world will have finally gotten over thinking this is their only “value add” (and will have found some even more sexy way to separate you from your money).

However, for the forseeable future, these multiple ratings systems will continue to live as non-interoperable data islands.  That means heavy “data generators” like me will have to make some pretty dopey (and unavoidable) decisions:

  1. When you find a new, even-more-attractive place to catalogue your consumption and how much you enjoyed it, do you abandon all the invested effort you put into the last one?
  2. If you don’t want to abandon all that “legacy data”, how will you migrate it to the new system? Just devote a freakin’ weekend to the prospect of clicking like a spastic lab rat, replicating each rating from one system to the other?  Or do you go even further down the rabbit hole and learn how to export the data (if that’s supported) from one and import to another – or go completely over the cliff edge into writing yourself some web-scraping scripts that pull the data by force out of systems that don’t have a supported import/export interface (API)?
  3. And if you actually *want* to maintain a presence in more than one system – e.g. if you find some ongoing benefit in having current presence in both Netflix and Flixster?  Well gods help you then – you’re screwed into a life of regular repeated self-inflicted punishment.

I’ve really lost my mind – thinking there’s got to be some way to actually pull off (3) without feeling like a character in a Kafka novel (and no, I’ve never read Kafka, so don’t crucify me for a misspoken cultural reference).

How’s About a Ratings Sync App?

Yeah, why the hell not?  Why not just burn the next years’ worth of weekends writing an extensible framework for us to be able to download, manipulate, upload and synchronize (i.e. manage and resolve the inevitable conflicts) the ratings data?  Isn’t is just like me, to think of doing some thankless job like this, in the hopes that some morsel of thanks comes through from some other hapless geek like me?

Sure, what the hell.

In fact, I’ve invested a whole bunch of time into this harebrained notion already.  Yes, I’ve written myself a bunch of code that attempts to provide an extensible, pluggable framework in which multiple “ratings” providers could be wired in, and between which synchronization could occur.

I actually dream that one day, users like me could:

  • Fire up this app
  • Connect to one of their ratings aggregating web sites
  • Download all ratings for whatever “things” are rated on that site
  • Select another compatible ratings web site (e.g. another books-rating web site if you just download book ratings)
  • Configure a translation between the two web sites (e.g. one site rates 1-5, the other site rates 1-10, so map 1 = 2, 2 = 4, 3 = 6, 4 = 8, 5 = 10; or if you’d prefer, 1 = 1, 2 = 3, 3 = 5, 4 = 7, 5 = 9)
  • Upload the translated ratings to the second site, thus synchronizing your ratings from one site to the next

This’ll require mapping out the APIs for each ratings-aggregating site, implementing an incredible flexible and robust local schema for the data, and figuring out all the different ways that different sites identify what to the human mind is an easy-to-identify product.

[And to think, this whole idea came about as a way to figure out how to migrate the ratings from my old Netflix account (to which I’ve still got access, but only just barely – due to the grace of an old housemate) to a new one that *I* own, and from which I could actually do Netflix Instant Watch in my own personality (and with my own IW queue).  I’m seriously considering just paying them for their subscription for the rest of my life, so I never have to lose those 2956 movie ratings.]

 

Anyone out there got a better idea?

Anyone else crazy enough to want to help out with this?

Anyone out there want to see this app see the light of day?

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. :)]

MindManager 7 ToDoList AddIn development Part 5: minimizing Object overhead between classes

I’ve created quite a puzzle for myself.  In writing an AddIn that calls functions from custom Classes, I am forcing the AddIn to pass in all the data that would be needed in the custom code.  I’ve added a few helper functions to the ToDoListBuilder class including GetAllTasks(), IsTaskCompleted() and IsTopicATask().  These are easy because I’m passing simple variables into each function.

However, I haven’t yet wired up the primary function that will be called from the AddIn’s mmCommand_Click() event, and I haven’t decided what I need to pass down from the button_Click() to the cascade of encapsulated functions, nor how far down to pass any object like a baton.

It seems like it’d be correct to pass the MMInterop.Application object into the ToDoListBuilder code, but in passing the Application object to a GenerateToDoListItems() method, I’ll have to pass the Application object to at least one further layer of called functions such as GetAllToDoListMaps().  Or will I?

Now that I’m looking around, the Connect class has a private applicationObject variable.  While it seems unlikely I’d be able to make reference to that Class’ private variable from a called Class, there should be no reason why I couldn’t create another private variable in the ToDoListBuilder class as soon as the GenerateToDoListItems() method is called, and then call on that Class-wide variable from then on.

Reducing Memory Footprint of Application Object(s) without Passing References Everywhere

Here’s how the code around the Application object comes out from the Visual Studio AddIn Template wizard by default:

    public class Connect : Object, Extensibility.IDTExtensibility2
{
private Mindjet.MindManager.Interop.Application applicationObject; private Mindjet.MindManager.Interop.Command mmCommand;
... }

Changing the first declaration to public from private makes that applicationObject object available outside of the assembly as well as within it, so that pointers (references) to the object don’t have to be explicitly passed around the code.

Assuming that nothing can affect the state of the applicationObject, there should be no reason not to declare it as “public”.  I’m not entirely naive though — I assume there’s reasons why you’d want to do one and not the other, but I have to believe that any code running in the MindManager application should necessarily want/try to use a single application object anyway.

Solution: Static/Shared modifier

After bashing my skull into the wall of my ineptitude for a few days, I finally dislodged a bone fragment of useful info: the “static” modifier.  Somehow this does what I had expected the “public” keyword to accomplish — makes it possible to access the variable from outside the class, without having to instantiate another copy of the object.

Open Questions

Now I’m left wondering two things:

  1. What’s the lowest level of accessibility that the object needs?  Does it still need to be public, or would protected or less be acceptable?
  2. Under what circumstances would it be inadvisable to use the static modifier?  I can imagine that in theory, anything that is supposed to represent multiple objects should not be marked “static”, but that still leaves a ton of room for interpretation (and for subtle mistakes in code that will bite me only later).

Aside: Registry entries in Setup project Aren’t Automatically Installed

Idiot assumption of the day: just because I entered the correct Registry settings in the Setup project, doesn’t mean that when I Debug the Solution those settings will be automatically added to the computer’s Registry.  (Sigh, sometimes I surprise myself with how dense I can be.)

So which “hack” would be better — should I hand-enter the Registry settings I need (which seems pretty lame) or should I build the Setup project and actually install this AddIn (which might end up leaving behind stuff that I’ll need to rip out later after I’ve rev’d the AddIn a few times)?

I guess I’m going down the path of hand-entering the Registry settings.  I don’t like doing this, and I really wish debugging MindManager AddIns didn’t require this lame step, but it looks like I’ve got no better option.

NullReferenceException: When will I ever learn?

I can’t believe the number of times I get caught by this seemingly predictable error:

        private System.Collections.ArrayList toDoListItems; // building list of all Tasks to be emitted as the items for the ToDoList
        private Mindjet.MindManager.Interop.Application applicationObject; // local instance of the Application object

        public System.Collections.ArrayList GenerateToDoListItems(MMInterop.Application application)
        {
            System.Collections.ArrayList toDoListMaps; // collection of all maps to be searched for Task topics
            applicationObject = application; // sets the local variable equal to the value of the passed-in parameter
            toDoListMaps = GetAllToDoListMaps(); // generate the collection of maps to be enumerated and searched
            toDoListItems = new System.Collections.ArrayList(); // declare this to avoid a NullReferenceException when it's assigned below
            
            foreach (MMInterop.Document map in toDoListMaps)
            {
                    toDoListItems.AddRange(GetAllTasks(map));
            }
            return toDoListItems;
        }

I really don’t get why I don’t have to declare the “new toDoListMaps” object, but I have to declare the “new toDoListItems”.  Is there something about creating the object inside the method that implicitly initializes it, but this implicit initialization doesn’t occur for objects that are created outside of the method?

And why does creating an int object not require initialization, but creating an ArrayList object does?  This might make sense to veteran coders, but my god it’s confusing for those of us just trying to get their first few apps out the door…

MindManager 7 ToDoList AddIn development Part 4: Fixing the Registry Settings in the Setup Project

The MM VS2005 AddIn wizard created a set of Registry settings for my project, and the friendly folks at Mindjet’s Dev Support team helped me fix a problem with my project (and in the process had to remove most of those Registry settings). Well, now we’re taking it back [anyone else remember Bono’s intro to Helter Skelter?].

It took me a bit of time to track down the hidden place where Visual Studio “hides” the Registry settings for a Setup project: right-click the Setup Project (“AddInSetup_en” in this case), choose View, Registry. There’s a stub for each hive, but not all of them contain actual Registry Keys or Values.

For my project, the only Key with any entries was HKLM\Software\Mindjet\MindManager\7\AddIns, with the Values (though different settings) that I’d noted earlier. The ProgID that it’s setting is ParanoidMike.ToDoListBuilder.AddIn which isn’t really representative of what I’m intending to release – ToDoListBuilder is just one portion of the functionality I want to provide (i.e. I’ve also spec’d out a ToDoListManager class that’ll be a peer to ToDoListBuilder).

Also, if I’m going to define a namespace rooted in ParanoidMike, I figure I should use the application name as the next-level-down, and then describe the actual code/app/add-in as the third-level-down. That gives me the namespace ParanoidMike.MindManager.ToDoListAddIn, so I’ve replaced the existing ProgID with this as the new ProgID (and .NET namespace for my source code). I deleted the old Registry entries that reference this ProgID, added the new ProgID and its entries, and also added the HKCR settings that were still missing when I published this article.

That results in the following Registry entries populated in the AddInSetup_en project:

Complete Set of Registry Entries for MM7 Add-in

HKLM\Software\Mindjet\MindManager\7\AddIns

Value

Data type

Setting

Description REG_SZ MindManager ToDoList AddIn for MM7 – Built by ParanoidMike
FriendlyName REG_SZ MM7ToDoList
LoadBehavior REG_DWORD 2

HKCR\ParanoidMike.MindManager.ToDoListAddIn

Value

Data type

Setting

(Default) REG_SZ ParanoidMike.MindManager.ToDoListAddIn

HKCR\ParanoidMike.MindManager.ToDoListAddIn\CLSID

Value

Data type

Setting

(Default) REG_SZ {33B60353-E8F3-4F38-98B6-41C7E5C6D32B}

Note: this little tip (HowTo: Create a Default Registry Value in Registry Editor) saved me a lot of trouble futzing around with these infrequently-created values.

Note: if you’ve used the MM7 AddIn template for Visual Studio, the CLSID above can be found in the Connect.cs file just under the ///

comment, like so:

GuidAttribute("33B60353-E8F3-4F38-98B6-41C7E5C6D32B")

Open Issue: ProgID versioning

There’s conflicting evidence on whether it’s best to create the ProgID using the “.1” suffix notation, so that the ProgID can be rev’d, or whether it’s just as good to leave it without the “.1” suffix and replace it upon every install (upgrade?). For the moment, until I stumble across anything better, I’m just going to leave it without.

Open Issue: ProgId in AddIn’s source code

After cleaning out the old entries from my computer’s Registry and testing this new set of code, I noticed that there was still the old ProgId referenced in the Connect.cs code:

    /// 
    ///   The object for implementing an Add-in.
    /// 
    ///    
    [
     CLSCompliant(false),
     ComVisible(true),
     GuidAttribute("33B60353-E8F3-4F38-98B6-41C7E5C6D32B"), ProgId("ParanoidMike.ToDoListBuilder.AddIn")
   ]

Since the add-in loaded and executed successfully, I’m now left to wonder what effect this ProgId entry is really supposed to have. Clearly it doesn’t affect the operations of debugging the code (i.e. hitting F5 from within Visual Studio 2008 and waiting for MindManager to launch), but I can’t imagine it’s just there for decoration either. It’s possible that, because I haven’t rebooted since I cleaned out the Registry and updated the AddInSetup_en project settings, there’s still enough of this information cached to allow the Add-in (with the same CLSID) to still work.

I’m taking no chances of a vaguely-specified error down the line, so I’ve updated this ProgId entry to the current value, but if anyone knows how these entries are related, I’d sure love to hear it.

MindManager 7 "ToDo List" Add-in development: Notes to Self

Notes to Self: Object Model stuff I Might Use in my Add-in

  • Application.ActiveDocument: the currently-displayed Map
  • Application.Visible: enables/disables the MM UI
  • DocumentObject.Guid, DocumentObject.ResetDirty: this seems like a generic object type within the MM object model — might be useful for manipulating a whole collection of different objects at once
  • Document.Attributes: collection of document’s custom attributes
  • Document.CentralTopic
  • Document.Guid
  • Document.IsModified
  • Document.Properties
  • Document.AssignMapMarkers(): assigns a map markers template to the document
  • Document.GetAttributes()
  • Document.Range(): returns a new Range collection of all the specified objects (topics, boundaries, relationships) in the document
  • Topic.AllSubTopics: subtopics connected to the topic
  • Topic.Attributes
  • Topic.Document: the document in which the Topic is found
  • Topic.Guid
  • Topic.Icons, Topic.UserIcons
  • Topic.IsFirstSibling: TRUE if the topic is the first in a set of siblings
  • Topic.IsMainTopic: TRUE if the topic is a child of the Central topic
  • Topic.IsSubTopic: TRUE if the topic is a child of another topic
  • Topic.Level
  • Topic.NextSibling
  • Topic.ParentRelationship
  • Topic.ParentTopic
  • Topic.SubTopics, Topic.UnfilteredSubTopics
  • Topic.Synchronization: ????
  • Topic.Task
  • Topic.Text
  • Topic.TopicLabel
  • Topic.TopicPrefix: sets the prefix for the topic — something to do with “numbering”
  • Topic.Type
  • Topic.AddControlStripType()
  • Topic.AddSubTopic()
  • Topic.CreateHyperlinkToTopicByGuid(): Cool! This could be used to hyperlink topics in ToDoList back to their original Topics. Might be useful to sync properties, attributes automatically.
  • Topic.GetAttributes()
  • Topic.NewTopicFinder(): helpful for traversing sub-topics from the current topic.
  • Topic.ResetDirty(): resets dirty bit of this and all subsequent objects. [Could be used to determine if a Topic has been changed since last “sync”.]
  • Topics.Add()
  • Topics.AddWithGuid(): creates new Topic with a given Guid
  • CustomProperty.Value
  • CustomAttributes.GetAttributeValue(), CustomAttributes.SetAttributeValue()
  • Task.IsDone
  • Task.IsValid
  • Task.Priority
  • Task.Topic: the parent Topic for this object
  • TextLabels.AddTextLabel()
  • Icons.AddStockIcon(), Icons.AddCustomIcon()
  • Icon.Delete()
  • Utilities.Execute

Notes to Self: Questions to Research

  1. What’s the difference between a CustomProperty and a CustomAttribute?
    • Attributes are persistent
    • Nick Duffill has more info here
  2. Is a TextLabel the same thing as a Text Marker?
  3. What’s the difference between a CategoryMarker, a CustomIconMarker and a CustomIcon?
  4. What Events are available to which to respond (such as Topic.Moved)?
    • Nick Duffill alludes to “topic-delete” and “topic-add” here
  5. Is there a way to find the current position among siblings for a Topic? Could there be a Property that indicates the Topic’s position?
    • There is the Topics.Item property…
  6. Can you get a handle to a Topic when you know its Guid? Do you need to be attached to its Document, or can you search all open Documents (e.g. searching through the AllDocuments or Documents collections)?
  7. Is there some way (e.g. derive a class from the Topic class) that would allow me to define a Property whose Value was equal to its 1-index position among its siblings?
  8. Does the Dirty flag get toggled automatically as soon as any change is made to the object? Or is this a flag that must be explicitly set?
  9. Does the Dirty flag inherit up the tree — i.e. if any Object in a Document has the Dirty flag set, does that implicitly “set” the Dirty flag on the Document as well?
  10. Can you reset the Dirty flag on a Document, and would that reset the Dirty flag on every Object in the Document all at once?
  11. Could the InsertCustomProperties event be used to automatically propagate a Property to another attribute on the Topic object (e.g. taking the Index of a Topic next to its ToDoList siblings and convert that to a custom ToDoListPriority property, or to use the Priority icon’s value as a ToDoListPriority value)?
  12. Is the SetCustomPropertyValue event the one that’ll let me trigger when the user changes the Topic’s ToDoListPriority value (either automatically, by moving the Topic to a different position among its List siblings, or manually, by typing the value in or selecting it from a pre-populated list) to propagate that value back to the source Topic?
    • If so, could I reset the Dirty flag on the Topic in the ToDoList, but leave open the possibility that the ToDoList overall is still “Dirty” and prompt the user to save unsaved changes?

Notes to Self: MM-specific Objects to Manipulate

There’s all these new Object types defined in the MM object model:

  • BusinessTypeRegistry/Business Topics (see Nick Duffill’s expert explanation of these Object classes here)
  • External Topics
  • Map Parts
  • MapShortcutCollections
  • Baselines
  • DocumentBars
  • Filter
  • Control Strips

Notes to Self: Code Tidbits

“use the Document.Range() method to iterate over all Objects” (Create and Use Custom Attributes)

Add Topics

Iterate over Topics (includes sample code for using the TopicFinder, an iterator)

Work with the Object ID (includes reference to FindByGuid() method that finds DocumentObjects by GUID in any Range, such as Document.Range)

Get an Object’s Type (which refers to an enumeration of DocumentObject that includes mmDocumentObjectTypeTopic)

MindManager 7 "ToDo List" Add-in development Part 3: Initial Working Code

Successful running of the add-in!  Once I added the required Registry settings, the add-in finally made itself known (through the MessageBox’es).  I learned that the HKCR settings are also necessary for other calls to succeed, but the essential behaviour of being recognized and loaded by MindManager was entirely dependent on having those Registry settings populated in the \Addins key.

At first I just used a .REG file to populate the Registry settings, following the model of the MM7 Sample Addin.  I had to do this as a side-effect of the work the helpful folks on the Mindjet “Dev Support” team did to rip out a bunch of “custom actions” stuff from my MM7 add-in solution, and this appears to have included all the Registry settings as well.  [Note: the Mindjet dev support folks are really helpful, and I’m not blaming them for this — the MM7 VS project template was (a) developed under Visual Studio 2005 not 2008 (which I’m using), and (b) was last updated last summer (and is no currently under active development I’d guess because the person who originally created it has since left the company — at least, that’s the usual way these things occur).

However, it’s not quite perfectly operating yet — just before the OnStartupComplete dialog box comes up, the following error is thrown by MM7:

image

According to this thread, this means that Addin ID must match the first parameter to applicationObject.Commands.Add().  In my code that parameter is “ParanoidMike.ToDoListBuilder.AddIn”, and my first guess is that the key under HKLM\Software\Mindjet\MindManager\7\AddIns needs to have the same name (which it currently doesn’t).  Upon changing it to match, I finally saw the Add-Ins Ribbon tab that I hadn’t been able to expose until now.

On to a little code…

First stage: Ribbon Button That Counts Tasks in Current Map

I wanted to use as much of the code from the MindManager7 Sample Add-in as possible… however somehow I ended up not using much or any of that.  This was my original plan:

  1. Create variable for MM application
    • Sample\MmxUtility\Helper.cs: MmxUtility.Helper.GetMmxMap()
  2. Beg/borrow/steal ribbon button code from existing add-in
    • Sample\MmxUtility\RibbonTabBase.cs (base class for creating a new Ribbon tab)
    • Sample\MmxUtility\ControlStripBase.cs
  3. Attach function to Ribbon button
  4. Create variable for Current Map
    • Application.ActiveDocument
  5. Find the Base Topic and enumerate
    • Sample\MmxUtility\Helper.cs: Helper.BaseTopicFinder()
  6. Create variable for Count
  7. Create For Each loop of Task objects, and count ’em
    • Sample\MmxUtility\Helper.cs (Topic attributes)

Instead, though, I ended up trying to separate out the UI from the data manipulation code, and leveraging the code I’d written from the MindManager DevZone article “How to Create a MindManager 7 Add-in Using C#“. 

This is what resulted:

        private void mmCommand_Click()
        {
            // Process the MindManager command and say hello
            MessageBox.Show("Hello world! (C#)", addInDisplayName);

            // Get the current Map
            Mindjet.MindManager.Interop.Document currentMap;
            currentMap = applicationObject.ActiveDocument;

            // Count the current Map's Tasks and display for the user:
            int count;
            ToDoListBuilder taskCounter = new ToDoListBuilder();
            count = taskCounter.countTasks(currentMap);
            MessageBox.Show("There are " + count.ToString() + " Tasks in this Map");
        }
    class ToDoListBuilder
    {
        public int countTasks(MMInterop.Document mindMap)
        {
            int tasksCount; // count of the number of Topics that are Tasks
            MMInterop.Range range; // just a collection of whatever we can find in the Document 🙂
            range = mindMap.Range(MMInterop.MmRange.mmRangeAllTopics, true);
            tasksCount = 0; // initialize to avoid error CS1065
            foreach (MMInterop.Topic topic in range)
            {
                if (topic.Task.Complete >= 0)  // this is intended to test whether the "topic" has Task attributes attached to it, and is how ResultsManager characterizes a Task
                {
                    tasksCount++; // increment this counter variable
                }
            }
            return tasksCount;
        }
    } 

The key magic in this code was the test for whether a Topic is a Task or not:

if (topic.Task.Complete >= 0) 

I’d spotted this approach in a macro that was published on ActivityOwner, but it seemed more complicated and indirect than should be necessary.  I wondered whether something like topic.Task.IsValid() would identify whether a topic had “valid” Task characteristics.  I looked into what documentation for this method was available, but the available info is pretty sparse.  If not for the generous help from the MindManager development community like Nick Duffill, I would’ve been forced to work through this by trial & error.

MindManager 7 "ToDo List" Add-in development Part 2: Adventures in Add-in Installation

MindManager 7 provides the ability (through the MindManager options menu) to inspect and enable/disable already-installed add-ins.  However, it’s not clear from the UI nor Help file how to install an add-in for MindManager.  The DevZone article indicates that once I’ve built the assembly it should be installed in MM7 automatically, but I’ve built it many times and it definitely doesn’t show up in the listed add-ins in MindManager:

If the code compiled successfully, your add-in DLL was created and registered with MindManager. At this point, you are ready to test your new add-in.

I’d posted a couple of requests to the MM7 developer user forum and that’ll probably give me some clues, but in the meantime I happened to find this blog article (Creating a MindManager 7 Add-in Sample) from last summer, and spotted this gem:

“Probably the most useful thing the wizard does is create a Setup project that carries the Windows Registry settings needed to let the MindManager application locate the Add-in component at load-time.  These settings are used by MindManager to discover and load selected Add-in components.  If they are wrong, your Add-in never makes  it onto the available Add-ins list.”

[That sharp sound you heard was my hand hitting my forehead]  Duh, indeed.

Registry Settings Are the “Key”

It appears that the critical piece of info I hadn’t found in the documentation (MM7 Help file, DevZone walkthrough) was the existence of the Registry Key HKLM\Software\Mindjet\MindManager\7\Addins\.  The add-in downloadable from the “Sample” blog article creates the following in that Addins key:

  • Key = MmxSample.Connect [i.e. the ProgID for the add-in]
    • Value: FriendlyName (REG_SZ) = Pandimonium[sic] Productions Mmx Sample Add-in
    • Value: LoadBehavior (REG_DWORD) = 2
    • Value: Description (REG_SZ) = A sample MindManager Add-In to dump map contents

As well (though I wonder if this is optional — at least while developing the add-in), the Setup project creates the following entries under HKCR (HKEY_Classes_Root):

  • Key = MmxSample.Connect
    • Key = CLSID
      • Value: (Default) (REG_SZ) = {925b5786-bf6f-4ac5-9df1-61ee50a815ca}
    • Value: (Default) (REG_SZ) = MmxSample.Connect

Since these Registry values are static, it appears that MindManager enumerates the keys under \Addins at each startup.  Therefore, I believe that just building the add-in assembly does not magically make MindManager aware of the add-in you’re developing.

So perhaps I can just populate my Registry settings by hand?  The AddInSetupen.vdproj file for my add-in’s setup project intends to set these values:

  • [\Addins] Key = ParanoidMike.ToDoList.AddIn.1
    • Value: FriendlyName (REG_SZ) = MM7TODOList
    • Value: LoadBehavior (REG_DWORD) = 2
    • Value: Description (REG_SZ) = MM7TODOList|Built by ParanoidMike|Version 1.0.0

Grrr

Oh hell.  I just went back to the DevZone walkthrough article, and the next thing (just beyond the point at which I abandoned the walkthrough) is this page that documents exactly the Registry settings I just unearthed.  Man, this is truly time to let it go for the evening…

One Open Question

My project’s Setup does not currently populate any HKCR settings!  Is this the cause of the “unrecoverable error” when building the Setup project?

MindManager developer resources

These are the various sources of information I’ve stumbled across so far that are useful for me (a budding MindManager add-in developer):

Resources related to Custom Properties

I anticipate leveraging custom Properties in my add-in, so I’ll want to dig into these articles when I get to that point: