Archive for Help
Intellisense Driven API Design
Posted by: | CommentsLike most framework developers, we put a lot of care and effort into what our public API looks like. We read some good books (even took their advice), looked at a lot of examples, and had some very long arguments about it. We employed a range of modern object approaches to maximize code reuse: Inheritance, interfaces, abstract classes… it all made good sense to us and appeared to work great.
Our beta customers weren’t as successful. Despite a real willingness on their part and frequent interactions with our team, they had a lot of trouble figuring out how to achieve the results they needed. Fortunately, the dialog back and forth gave us a lot of insight into where people were getting tripped up. The a-ha moment was one comment:
I don’t think your notes were bad, I just think the manual event process is different than I expect from just poking around. Can you even call it the way I am trying? There were a couple of syntax attempts that looked ok to me, but didn’t work.
Novices of an api often just try to use the objects.
That got us started down a new path: How do we make it as obvious as possible from the namespace what object they should use? From just what you can see with Intellisense, can they find what method(s) to use and why? Making that happen became our mantra – Intellisense Driven Design. A moderately experienced developer should be able to figure out what to do just from what’s visible using Intellisense in Visual Studio.
To meet this goal we made a number of changes:
- Ruthlessly get rid of classes in the namespace: Every class was an opportunity for confusion where someone asks What is that and why do I need it?
- Reduce usage patterns: We previously supported about five patterns of using the objects to suite a range of personal coding preferences. We reduced this two the bare minimum of two patterns: One declarative and one entirely programmatic.
- Separate things that are separate: We were using inheritance to link together several things that we internally treated similarly. At the API layer that confused users into thinking that the items were more interchangeable than they were. We redesigned them to be more clearly distinct to prevent the objects from being interchangeable at the compiler level while preserving common usage patterns.
- Expose concrete classes: While inheritance and interfaces maximize code reuse within the API itself, they tend to generate a lot of noise in the namespace.
- Make the API completely thread safe: We expect users to want to use our API in multithreaded applications, but many developers aren’t fully versed on how to correctly handle shared data. We changed the API to make it impossible to use in a thread unsafe manner.
To get this done we had to do some counter-optimizations. The biggest issue in our way was that we reused the same classes to write and read data, however our customers would only use the API to write data. The desire to reuse the classes was core to the additional complexity, driving extensive use of inheritance and interfaces. It also necessitated exposing a large number of methods that were really only suitable for internal use.
To achieve the goals we wanted for usability we ended up creating a new API layer exclusively for our customers, even though that ended up creating a lot of duplication within our library. In retrospect this seems almost obvious: Customers don’t care about how clean our internal object model is or how much work it took, they care about having it be as easy as possible to understand.
The second counter optimization was that a few of the additional usage patterns were oriented at providing the highest possible performance for customers with certain scenarios. This turned out to be an example of pre-emptive optimization; while they did improve the performance by around 10% the API was already fast enough even in its slowest usage scenario that such improvements were irrelevant in the real world.
The results
- The number of exposed classes was reduced from 79 to 22. Of the 22, 7 are attribute classes used for Declarative definition leaving only 15 classes users would ever need to code against.
- The number of lines of code required to implement the average usage patterns was reduced by 50%.
- Peak performance was reduced by 10%, but average performance was the same. Despite the code duplication, the final binary ended up only 40kb larger than it was before.
What’s it all really mean?
Before the Refactoring
/// <summary>
/// Snapshot cache metrics
/// </summary>
public static void RecordCacheMetric(int pagesLoaded)
{
CustomSampledMetricDefinition pageMetricDefinition;
CustomSampledMetric pageMetric;
CustomSampledMetricDefinition sizeMetricDefinition;
CustomSampledMetric sizeMetric;
//since sampled metrics have only one value per metric,
//we have to create multiple metrics
MetricDefinition workingDefinition;
if (Log.Metrics.TryGetValue("Example", "Database.Engine", "Cache Pages", out workingDefinition) == false)
{
//doesn't exist yet - add it in all of its glory
pageMetricDefinition = new CustomSampledMetricDefinition("Example", "Database.Engine", "Cache Pages", MetricSampleType.RawCount);
pageMetricDefinition.Description = "The number of pages in the cache";
pageMetricDefinition.UnitCaption = "Pages";
}
else
{
//it already exists- just need to type cast it.
pageMetricDefinition = (CustomSampledMetricDefinition)workingDefinition;
}
if (Log.Metrics.TryGetValue("Example", "Database.Engine", "Cache Size", out workingDefinition) == false)
{
//doesn't exist yet - add it in all of its glory
sizeMetricDefinition = new CustomSampledMetricDefinition("Example", "Database.Engine", "Cache Size", MetricSampleType.RawCount);
sizeMetricDefinition.Description = "The number of bytes used by pages in the cache";
sizeMetricDefinition.UnitCaption = "Bytes";
}
else
{
//it already exists- just need to type cast it.
sizeMetricDefinition = (CustomSampledMetricDefinition)workingDefinition;
}
//now that we know we have the definitions, make sure we've defined the metric instances.
if (pageMetricDefinition.Metrics.TryGetValue(null, out pageMetric) == false)
{
pageMetric = pageMetricDefinition.Metrics.Add(null);
}
if (sizeMetricDefinition.Metrics.TryGetValue(null, out sizeMetric) == false)
{
sizeMetric = sizeMetricDefinition.Metrics.Add(null);
}
//now go ahead and write those samples....
pageMetric.WriteSample(pagesLoaded);
sizeMetric.WriteSample(pagesLoaded * 8196);
}
After the refactoring
And with the new API changes:
/// <summary>
/// Snapshot cache metrics
/// </summary>
public static void RecordCacheMetric(int pagesLoaded)
{
SampledMetricDefinition pageMetricDefinition;
SampledMetricDefinition sizeMetricDefinition;
//since sampled metrics have only one value per metric,
//we have to create multiple metrics
if (SampledMetricDefinition.TryGetValue("Example", "Database.Engine", "Cache Pages", out pageMetricDefinition) == false)
{
//doesn't exist yet - add it in all of its glory.
//This call is MT safe - we get back the object in cache even if registered on another thread.
pageMetricDefinition = SampledMetricDefinition.Register("Example", "Database.Engine", "cachePages", SamplingType.RawCount, "Pages", "Cache Pages", "The number of pages in the cache");
}
if (SampledMetricDefinition.TryGetValue("Example", "Database.Engine", "Cache Size", out sizeMetricDefinition) == false)
{
//doesn't exist yet - add it in all of its glory
//This call is MT safe - we get back the object in cache even if registered on another thread.
sizeMetricDefinition = SampledMetricDefinition.Register("Example", "Database.Engine", "cacheSize", SamplingType.RawCount, "Bytes", "Cache Size", "The number of bytes used by pages in the cache");
}
//now that we know we have the definitions, make sure we've defined the metric instances.
SampledMetric pageMetric = SampledMetric.Register(pageMetricDefinition, null);
SampledMetric sizeMetric = SampledMetric.Register(sizeMetricDefinition, null);
//now go ahead and write those samples....
pageMetric.WriteSample(pagesLoaded);
sizeMetric.WriteSample(pagesLoaded * 8196);
}
But wait there’s more!
That doesn’t seem too impressive until you realize that with the new API it’s possible to do the following, still in a thread-safe manner:
/// <summary>
/// Snapshot cache metrics using fewest lines of code
/// </summary>
public static void RecordCacheMetricShortestCode(int pagesLoaded)
{
//Alternately, it can be done in a single line of code each,
//although somewhat less readable. Note the WriteSample call after the Register call.
SampledMetric.Register("Example", "Database.Engine", "cachePages", SamplingType.RawCount, "Pages", "Cache Pages", "The number of pages in the cache", null).WriteSample(pagesLoaded);
SampledMetric.Register("Example", "Database.Engine", "cacheSize", SamplingType.RawCount, "Bytes", "Cache Size", "The number of bytes used by pages in the cache", null).WriteSample(pagesLoaded * 8196);
}
One line of code per metric, only two lines of code down from 11 (excluding braces). More importantly, the original implementation wasn’t thread safe; you’d have to use an external lock in your own code over each metric.
Closing Thoughts
Time will tell how successful we were with our goals. You can check out the current API documentation online, or download the product and try it out yourself. Our internal unit tests and examples give us a lot of confidence that it’s much improved, but the ultimate test is our customers. While we’re a huge believers in documentation, even we charge in and see how far we can get before reading anything, so why would we expect more from our customers? We were very lucky to be able to correct this before we shipped the first version and were married to supporting it for backwards compatibility.
Finally, this highlights the value of leaving time for external beta testing. We did three rounds of external beta testing, and each provided a wealth of essential information about our target market, what worked, and what didn’t. While each of these rounds were invitation only, with new participants in each round we discovered what we needed to know to make the best software. Regardless of where you are on the Agile development spectrum, if you’re making an API you can be sure that no matter how well you think it through your first users will point out key flaws. Make sure you can address them before you are committed to preserving your mistakes for all time.
Good Help is Hard to Find
Posted by: | CommentsOne of the greatest parts of modern development for me is the combination of Intellisense and F1 help for an API. For a really great API you can usually figure things out from naming and the very short bits of help in Intellisense what to do and why. But, anything that requires more than one call to complete benefits from deeper help. It’s great to be able to, right in the moment you’re puzzling “huh, why this and not that?” press F1 and *poof* there’s a page of help about right where you are with links to go deeper, higher, or over to where you need to be.

This is why it was a top priority for us to get Visual Studio integrated help ready even while we were still in Beta. Since we’re also big fans of Continuous Integration, it wasn’t long after we got our build server set up we were configuring automatic API help generation as well. Initially we started with Sandcastle, but in the end we get better results from a commercial product, Document! X. It was also a lot easier to set up and use, which in our case alone was worth spending some money to save a lot of time.
So the final step to good help was integrating the HTML Help 2 output into the installation so that the Windows Installer produced by our build process would install the Visual Studio integrated help. Here’s where we’ve come to a screeching halt. The problems start with what it takes to register HTML Help during an installation. There are basically two approaches:
- Use a registration program, such as H2Reg. This is also known as the Bad way. In the end, you’re relying on an executable fired up on the target system at a key point in the installation to make it work. If this is on Vista it may not run with Admin access. You may run into problems with auto-deployment in enterprises where they’re expecting everything to be done by Windows Installer itself. In the end, you really are better off not going this way if you can.
- Create a custom mangled Merge Module for your help. This is the official Good way. The problem is that it isn’t enough to just put your help files, or even any set of files, directly into the merge module. Microsoft has published two approaches, and both are flavors of “re-edit the final merge file after the fact to make it all work”.
For the highest compatibility and best customer experience, it was very important to us to have a single MSI file that could work in an enterprise scenario. To this end, Microsoft published the directions on how to create a correct merge module for help, including how it has to be manually modified after generation to fix it. These directions are complicated and a challenge to get done correctly. Fortunately, Microsoft created and shipped a few years ago the Help Integration Wizard to solve all that.
Running through the wizard, the biggest problem was knowing what magic values needed to be set, and what values shouldn’t be changed to make it all work. The documentation really isn’t of much help here; while Microsoft dramatically improved it for 2008 that appears mostly to be about adding screen shots of each page of the wizard, not giving any guidance about what to put in it.
There’s Always a Catch
All appeared rosy: You run through the wizard and with some cross referencing come up with what appears to be the right magic values, and you’re set. What comes out the other end is an installation that actually seems to do what it should: There’s your help right in MSDN with the rest of it, F1 ready to go.
My first clue that all was not well was when I noticed that the merge module created by the Installation wizard isn’t set up to check everything it needs into source code control. Indeed, if you check it into source code control very little is put in – none of the critical files that it actually generates during the wizard process. Not good. Also, it uses paths to things that aren’t transferable between 64 bit and 32 bit versions of windows (Best guess: The wizard is written in .NET and runs as a 64 bit process, the rest of Visual Studio doesn’t so you get the “real” 64 bit paths to files in the 32-bit area of Program Files.).
Some mild work with manually putting things into source code control later, it now builds repeatedly. And everything works.
Right up until you change the help file. The next build when the help file changes, the resulting install runs… and does nothing. Nada. Quiet as a church mouse. No indication of failure, just… no help.
After some experimentation I determined that it appears you have to re-run the wizard every time you change your help file. Now, nowhere in the documentation could I find any indication on when you have to re-run the wizard: Every time you change anything in the help file? When you change things in the Table of Contents (and think about it – you write content that doesn’t show up in the TOC?)? Adding to this treat is that the wizard can’t be re-run automatically (e.g. scripted) and it creates a new module every time it runs, requiring I modify my installation to include that module instead of the original module. Now see why it doesn’t bother putting the files into source code control? They’re output files.
Seriously? This is the best we can do?
Since the Help Integration wizard has to be re-run each time the help file changes, and it has to be run by a person it can’t be in our automatic build process. That means there’s not much point in having the help build in our automatic build process. In the end, I strongly suspect we’re going to have to go back and figure out how the hard way works and write our own utility to munge the merge module so that we can automate this into the build process.
The irony is that what’s taken the most time for us in this affair is the lack of documentation on how the help integration wizard works: When it has to be re-run, what exactly it expects you to put into your resulting installation (there are some mysterious merge module dependencies and oh yeah, you get some warnings when you build your installation. The documentation in one area says they can be safely ignored if your install works. Nice bar.)
It’s all solvable: This can be made to work, and it’s not the worst problem we’ve ever faced. But in the good old days of COM you just had the help file in the same directory as the binary and that’s it, you’re good to go. Now we’ve improved things and it takes a hand-crafted binary file and adds minutes to the install time for users. The point is that it really didn’t need to be this way – A major advantage of having tight control over the platform (and it’s hard to argue Microsoft doesn’t have that between Visual Studio and .NET) is making everything easy.
Apple would have done it.

