Code Examples‎ > ‎

HV2Viewer

Description: 
Demonstrates coding a standard Help Viewer window with TOC/Index/Search for viewing HV2 catalogs.
Requires: 
Visual Studio 11 (for .NET Framework 4.5 and Help Viewer 2.0)
Code Language: 
C# 

Introduction

This example demonstrates how to build a standard help viewer window that reads HV2 catalogs, using the Help Viewer 2.0 API (Microsoft.VisualStudio.Help.Runtime.dll COM interface).

We won't be talking about the chrome, just the main help API calls at the heart of the viewer.

Setup

Open the solution HV2viewer.sln (alternatively open HV2Everything.sln and make the "HV2Viewer" project the start up project)


The main executable project "HV2Viewer" uses 2 other DLL assemblies "Helpware.Misc" (our non-help related support code) & "HV2Lib" (our HV2 related support code for rendering etc).

Both HV2Viewer and HV2Lib require references to the following 2 x Help Viewer 2.0 DLLs.  
Actually only HV2Lib uses the second DLL solely for the rendering HTML.
(in VS 11 we right-click each project's "References" folder and select "Add Reference" and added these DLLs)

C:\Program Files\Microsoft Help Viewer\v2.0\Microsoft.VisualStudio.Help.Runtime.dll
C:\Program Files\Microsoft Help Viewer\v2.0\Microsoft.VisualStudio.Help.dll

  • Microsoft.VisualStudio.Help.Runtime.dll -- This is the HV2 core API which allows you to open catalogs and read catalog data.
  • Microsoft.VisualStudio.Help.dll -- This API allows you to correctly render VS help topics. For non-VS topics you can ignore this and just use the renderer code provided in HV2Lib DLL.

Windows 8 Runtime

To use the Windows 8 Help run-time instead of VS 11 Help run-time:
  1. Remove the 2x HV2 DLL references (listed above)  from both HV2 projects.
  2. Add the equivalent Windows 8 Help DLL (stored in the GAC). This should be registered as a local COM server.
  3. Adjust the HV2 Namespaces (see below).

    Windows.Help.Runtime.dll
Note that Windows 8 does not provide an equivalent to Microsoft.VisualStudio.Help.dll for rendering support. In this case you will need to comment out the one VS Help render call in our code examples (which will be obvious because it will cause a compile time error).


HV2 Namespaces

To open and read catalogs you will need this namespace:

using Microsoft.VisualStudio.Help.Runtime;

or if you are coding using Windows 8 help runtime DLL

using Windows.Help.Runtime;


For the one place where we reference VS 11 renderering method you need:

using Microsoft.VisualStudio.Help;

If you are coding using the Windows 8 help runtime, there is no equivalent rendering DLL. 
However, we provide code in the HV2Lib assembly to renderer topics. More on this later.


Opening Catalogs

The API can open managed & unmanaged catalogs as well as .mshx file catalogs.
There are 2 main classes we need to access (Catalog & CatalogRead). Most of the other class/enums are used by the methods in these classes.

First thing to do instantiate these 2 important classes. 

private Catalog _catalog = new Catalog();
private CatalogRead _catalogRead = new CatalogRead();

To open a catalog we pass the catalog directory path and locale to the Open() method.
The locale parameter is a prioritized list of locales, but we normally want to pass in one specific locale.

String catalogPath = @"C:\ProgramData\Microsoft\HelpLibrary2\Catalogs\VisualStudio11";
String locale = "en-US";
_catalog.Open(catalogPath, new String[] { locale });

To open a .mshx file we use the OpenMshx() method.

String catalogPath = "c:\somePath\helpfile.mshx";
_catalog.OpenMshx(catalogPath);

Remember to always close catalogs when you are finished with them. In this app we check if the catalog is open and close it before opening another catalog. And also close any open catalog on shutdown (although the dispose would do that for you when the object is destroyed by the system).

if (_catalog.IsOpen)
    _catalog.Close();

Use the Open Catalog command in the Toolbar to access these commands.
Below the menu are a list of VS registered catalogs. A hover tip displays the registry info for the catalog registration.
See Catalogs to find out about catalog registration.


Return Value

At the time of writing most method don't return errors. If there is a problem (file not found, or invalid catalog) you will catch an exception.


Table Of Contents

A HV2 table of contents can contain millions of items, so we build it incrementally as nodes are expanded.
To get the the [+] gadget to appear on unexpanded nodes we add a dummy node under the unexpanded node.
The TOC is hosted in a UserControl which we parent into the Content navigation tab. When you click a TOC node there is a call back to ping a handler in the main form code. You can explore this code at your leisure.  

Here are the main HV2 API calls for building a TOC.

This gives us the root nodes of the TOC.

ITopicCollection topics = _catalogRead.GetTableOfContents(_catalog, "-1", null, TocReturnDetail.TocRootNodes);

This one gives us the immediate children of a particular topic.

ITopicCollection topics = _catalogRead.GetTableOfContents(_catalog, topic.Id, null, TocReturnDetail.TocChildren);

And that's all we really need to build a TOC.

The GetTableOfContents() method returns a collections of Topic objects, according to the parameters passed in. 
  • Parameter 1: An open catalog object (we created that in the Catalog.Open() call).
  • Parameter 2: A topic ID (not applicable in the TocRootNodes call). Use "-1" to represent the TOC root node.
  • Parameter 3: A Filter which is a list of Name\Value pairs. We are not using filters in this demo. We want the full TOC so we pass null (no filtering).
  • Parameter 4: Specifies the type of TOC info we want returned (see TocReturnDetail enum).

TocReturnDetail enum

TocReturnDetail.TocAncestors  returns a list of parent topics of the specified node up to the root.
TocReturnDetail.TocChildren  returns a list of child topics of the specified node.
TocReturnDetail.TocDescendants  returns a list of every topic under the specified node. 
TocReturnDetail.TocRootNodes  returns a list of all level 0 nodes (children of TopicID="-1").
TocReturnDetail.TocSiblings  returns a list of all nodes at the same level with the same parent. 

Topic class

ITopicCollection is a collection of Topic objects. Here we are getting all Topics from the collection.

ITopicCollection topics = _catalogRead.GetTableOfContents(_catalog, "-1", null, TocReturnDetail.TocRootNodes);

for (int i = 0; i < topics.Count; i++)
{
    topics.MoveTo(i);
    Topic topic = (Topic) topics.Current;
    ....
}

There is also topics.MoveNext(); that can be used if you are iterating with a foreach(...) { }  loop.

The Topic class has very handy members. Here an example of the contents:

topic.Category                
topic.ContentFilter           Visual F#
topic.ContentType             Reference
topic.Description             Returns a string by concatenating...
topic.DisplayVersion          
topic.Id                       489CF6E9-E0A0-457A-9E9B-BF630A40A25B
topic.Locale                   en-US
topic.Package                 Visual_Studio_21800792_VS_100_en-us_1.mshc;\R402.htm
topic.ParentId                 A5FDA9CD-D71F-4271-A6A4-AB4CAA0BE550
topic.TableOfContentsHasChildren False
topic.TableOfContentsPosition 10
topic.Title                   String.replicate Function (F#)
topic.TopicLocale             EN-US
topic.TopicVersion             100
topic.Url                     Visual_Studio_21800792_VS_100_en-us_1.mshc;\R402.htm
topic.Vendor                   Microsoft

Note especially...
  • topic.TableOfContentsHasChildren tells us if we can expand the node.
  • topic.Title gives us the node text.
  • topic.ParentId gives us the topic ID of the topics patent topic in the TOC.

Index 

Again this is very simple. The VS catalog can contain over a million keywords so we are using a ListView control in Virtual mode.

We get a collection of Keywords by passing the _catalog object into GetKeywords() method.

IKeywordCollection keywords = _catalogRead.GetKeywords(_catalog, true);

    The true parameter enables some caching to speed up loading.

Keyword Class

We can access each Keyword object by iterating through the collection.

for (int i = 0; i < keywords.Count; i++)
{
keywords.MoveTo(i);
keyword keyword = (Keyword)keywords.Current;
....
}

There is also keywords.MoveNext(); that can be used if you are iterating with a foreach(...) { }  loop.

The keyword object has these handy members:

 keyword.DisplayValue holds the keyword display string.
 keyword.IsSubkey tells us if we need to indent the keyword.
 keyword.Topics gives access to the list of result topics associated with the keyword.

Word Wheel

We want to type in the edit box (above the keyword list) and select the closest match in the list (the keyword starting with that text).

The MoveToKeyword() method returns the index of the List item we want to select and scroll into view

int i = _helpKeywords.MoveToKeyword(text);   //where text is the text typed
if (i >= 0 && i <= _helpKeywords.Count)
{
   ...


Search

Search is also straight forward.

Here GetSearchResults()  returns a list of topic hits.

String query = "StringBuilder";  
HelpFilter filter = null;      // adv filtering not required (we want all results)
int pageSize = 50;             // number of items per page to return
int pageNumber = 1;            // which page of results to return 
int _totalAvailableHits = 0;   // total number of hits returned 

ITopicCollection topics = _catalogRead.GetSearchResults(_catalog, query, filter, SearchOptions.None, pageSize, pageNumber, out _totalAvailableHits);

And again we can iterate through the returned topic list (see details for ITopicCollection and Topic above).

These Search options can be OR'd together if required.

SearchOptions.None no options.
SearchOptions.OrSearchOverride changes search terms "x y", normally interpreted "x AND y",  to "x OR y".
SearchOptions.SearchTermHighlight adds highlighting html tags to search result text.

Note: The API code demo demonstrates the options.

More Search Options

Note that special search prefixes (as it's not clear from the demo) can be concatenated and prefix any search term.

So when searching for "String AND Lock" this is also valid: "code:c#:keyword:String AND title:Lock".



ms-xhelp:/// Protocol  & Rendering

In HV1 the protocol ms-xhelp:/// was associated with the Agent tray application, which would fully render the returned topic for the default browser or help viewer.

HV2 and HV1 need to co-exist, so in HV2 we associated the protocol with just our application using a Pluggable Protocol. Once installed any URL starting with ms-xhelp:/// protocol directed at the embedded web browser, will cause an event to fire asking for the stream of the required HTML file or asset (image).

The Pluggable Protocol code (similar to that used by VS 11 HlpViewer.exe) is included in HV2Lib.DLL. 


Here's how to use our example MsxhelpProtocol object.

private MsxhelpProtocol msxhelpProtocol = new MsxhelpProtocol();

and whenever a help catalog is opened we tell the object so it knows where to read help topics from.

MsxhelpProtocol.Catalog = _catalog;

We tell the object which render to use:

MsxhelpProtocol.RenderUsingVS = TRUE;

If you use the VS render you will get better result for VS content but the VS render is only available if VS 11 is installed (as we talked about in the introduction). Alternatively set this FALSE (the default) and use the supplied rendering code. 

The advantage of using the supplied rendering code is 
  1. It works if VS 11 is not available (eg. the Windows 8 OS).
  2. You can extend it (you have the code) to support embedded Adobe Flash, Shell Execute or any other technology.

About Rendering

Try flipping between the VS Renderer (checked) and the Custom Rendering (unchecked), using the options menu. 
If you were building an actual embedded viewer, you would typically choose one or the other.
  • VS Rendering does the best job with the complexities of rendering VS help content. But VS 11 must be installed because of the dependency on Microsoft.VisualStudio.Help.dll
  • The Custom Rendering does not require VS to be installed. You also have full source code (see example code) to make changes (eg. Add Flash support; Add execute support).



The "View Unrendered Source" command shows the raw source straight from the .mshc file. Use the WebBrowser "Viewer Source" command to view the rendered source. The differences you see are what the rendering code adds. At a minimum the rendering code needs to expand all topic links (CSS, Images, JS etc) to use the ms-xhelp:/// protocol, that way our handler will be asked to serve up the stream of the asset from the current catalog.

Brief Code Walk Through 

The pluggable protocol handler (receives requests for catalog assets and indexed HTML files) is here...

<HelpViewerProject\HV2Lib>\PluggableProtocol\HelpPluggableProtocolHandler.cs

Once the Pluggable Protocol is installed (see new MsxhelpProtocol() above)  we can do the following and our handler will server up the catalog topics and assets.

String url = "ms-xhelp:///?method=page&id=xxx&locale=en-US&vendor=Microsoft&topicVersion=&topicLocale=EN-US"

webBrowser1.Navigate(url);

The URL above says load the catalog page with topic Id=xxx. If you can provide all parameters do so. Otherwise the following (with no-ti-breakers) will normally work just as well.

String url = "ms-xhelp:///?method=page&id=xxx"
webBrowser1.Navigate(url);


Next the Handlers HelpPluggableProtocolHandler.cs receives a request to serve up the help topic from the catalog.

The handler palms the rendering off to either the VS render or our custom rendering code. We examine the szURL (topic file, image file, CSS file, js file etc) and returns a stream from the current catalog.

if (MsxhelpProtocol.RenderUsingVS) //Requires VS 11 DLL(Microsoft.VisualStudio.Help.DLL)
{
    if (_vsRenderer == null)                     
        _vsRenderer = new TopicRenderer();
    _stream = (Stream)_vsRenderer.ProcessLink(szURL, (ICatalog)MsxhelpProtocol.Catalog, null/*_renderParameters*/);
}
else // our custom code
{
    if (_customRenderer == null)
        _customRenderer = CustomRenderer.Create();
    _stream = _customRenderer.UrlToStream(szURL);
    _stream.Position = 0;
}


That's it!  The handler first receives the topic URL (as sent to webBrowser1 above). We read the topic from the catalog and tweak all the links so they use the ms-xhelp:/// URLs and point to the correct location in the catalog file. If rendered correctly, the next messages that come though will be requests for the assets (JS, CSS, image files etc).


Comments