Context Ids in Merged Help (2)

Last update: 24-Aug-2005 (Added Solution 3)
Author: MVP Rob Chandler

Continues from how_tomerge.htm#ContextHelpIDs.

Introduction

Defining context mappings (see how_to_context.htm) works fine in a standalone CHM, but in a merged help system we can run into problems when trying to open a Slave topic though the master CHM.

We could make all the slave CHMs use the Master TOC and merge (shown in Step 3 of the Demo Project). That way all CHMs look the same, and we can open Slave1.CHM directly to access Slave1 topics or context Ids. The problem here is that each CHM has its own favourites and window position info. Also we really just want the application to have to deal with the master CHM.

Note: Since the latest Windows critical update (hhctrl.ocx 5.2.3735.x) some older techniques for making context calls to slave topics have broken. See how_tomerge.htm#ContextHelpIDs to see the older solutions. Some of these techniques still work fine if an index is not require.

Here are some new idea for 2004. Hopefully there is a solution that will help you.

  1. Solution #1 - Mappings on the Help Side.
  2. Solution #2 - Mappings on the Application Side, using a Redirection file.
  3. Solution #3 - Mapping on the Application Side, every CHM is a Master.

 

Solution #1: Mappings on the Help Side

By using a redirection file in your Master CHM your application can make context calls to any topic (in master or slave) while accessing only the Master CHM. The application requires no modification.

Master.HHP

[ALIAS]
IDH_MASTER_TEST1=index.htm
IDH_MASTER_TEST2=linkdemo.htm
IDH_SLAVE1_INDEX=redirect.xhtm#IDH_SLAVE1_INDEX
IDH_SLAVE1_ANOTHER=redirect.xhtm#IDH_SLAVE1_ANOTHER
IDH_SLAVE2_INDEX=redirect.xhtm#IDH_SLAVE2_INDEX
IDH_SLAVE2_ANOTHER=redirect.xhtm#IDH_SLAVE2_ANOTHER

[MAP]
#define IDH_MASTER_TEST1 1
#define IDH_MASTER_TEST2 2
#define IDH_SLAVE1_INDEX 110
#define IDH_SLAVE1_ANOTHER 111
#define IDH_SLAVE2_INDEX 120
#define IDH_SLAVE2_ANOTHER 121

[FILES]
redirect.xhtm
...

As you can see this Master .HHP file still contains the normal HTML Help mapping info. However notice that the URLs for the slave topics (in the ALIAS section) point to a redirection file stored in the master CHM. Each URL passes a unique string ID to the redirection file via a bookmark.

IDH_MASTER_TEST1 & IDH_MASTER_TEST2 items simply demonstrate that no special attention is needed for those topics (eg. index.htm & linkdemo.htm) residing within the master CHM file.

Redirect.xhtm

<html>
<head><title>Redirect Page</title>

<script>
  var Code = window.location.hash.substring(1);
  var URL= "";

  if (Code == "IDH_SLAVE1_INDEX") URL="its:slave1.chm::/s1_index.htm";
  else if (Code == "IDH_SLAVE1_ANOTHER") URL="its:slave1.chm::/s1_another.htm";
  else if (Code == "IDH_SLAVE2_INDEX") URL="its:slave2.chm::/s2_index.htm";
  else if (Code == "IDH_SLAVE2_ANOTHER") URL="its:slave2.chm::/s2_another.htm";

  if (URL != "")
     window.location.replace(URL)
  else if (Code != "")
     alert("Redirection code not defined: "+ Code); 
</script>

</head>
<body>
</body>
</html>

Notice the above redirection file contains the real mapping info.

window.location.hash.substring(1); returns the bookmark (anchor) of the URL and stores it in variable CODE. We then set the URL variable according the value in CODE.

window.location.replace(); simply takes you to the URL while replacing the current page. The Replace part removes the redirection file from the browsers history list.

alert("Redirection code not defined: "+ Code); This just pops up a debug message if an incoming bookmark is unknown.

More Detail:

  1. Funny filename: Redirect.xhtm
    Notice that I have added a "x" before the "htm" file extension to stop the compiler from parsing this file for Full-text search info. If the HH compiler finds a ".h" in the file name it assumes it is a HTML file and parses the file for full-text search terms (that's why binary files like file.h00.gif can break the compiler).
     
  2. XXX=redirect.xhtm#its:slave1.chm::/index.htm
    Why not use this notation and keep all the URLs together in the HHP? This also simplifies the Redirection file to just a generic..
    <script>window.location.replace(window.location.hash.substring(1))</script>

    Unfortunately the HH Compile seems to parse these mapping URLs (in the .HHP) from right to left. You need to use very simple bookmarks that do not contain punctuation such as ":" or "." otherwise the links take you directly to the bookmark URL and ignore the reference to the redirection file.
     
  3. Compilation Errors
    The HH compiler will complain about the bookmarks in the ALIAS section of the .HHP file. Just ignore these warnings. There is nothing wrong with the compiled CHM.
     
  4. Additional - You may not need to use the "its:" protocol prefix. Not needed in the modern IE browser (in fact I've been told the IE7 Beta2 does not like it at all).

Tip: Make sure all window types use the $Global_ prefix to force all CHM help into the same window.
 

 

Solution #2: Mappings on the Application Side, using a Redirection file

I find it best to avoid the HH_HELP_CONTEXT Context call completely. Instead of defining ContextID/URL mappings on the help side, store them on the application side and use the more reliable HH_DISPLAY_TOPIC call.

  HtmlHelp(0, "c:\Help.chm", HH_HELP_CONTEXT, 1001);
  HtmlHelp(0, "c:\Help.chm::/Intro.htm>Mainwin", HH_DISPLAY_TOPIC, 0);

We keep all our mapping info in a separate INI file, that way bug fixes do not require a rebuild of either help or executable. Also the INI file can be maintained by either the tech-writers or programmers.

eg. Mapping.ini

[MAIN]
1000=its:master.chm::/index.htm
1001=its:master.chm::/redirect.xhtm#its:slave1.chm::/slave1Topic.htm
...

When a help event occurs your application code simply takes the context ID of the current dialog or page, and looks up the corresponding help URL in the INI file. It then opens that URL using the HH_DISPLAY_TOPIC command.

Note that we open slave topics via a redirection file in the Master CHM. The target slave URL is passed as a bookmark (anchor). Thus our application only has to open the Master CHM, never a Slave.

Redirect.xhtm

<html>
<head>
<title>Redirect Page</title>
<script>
window.location.replace(window.location.hash.substring(1));
</script>
</head>
<body></body>
</html>

Very simple. When Redirect.xhtm is opens it grabs the URL from the bookmark and immediately goes to that location. Thus our application can make the following call and the master will open but the slave 1 topic will be displayed.

The redirect file is never seen. The .replace statement stops the Redirect.xhtm URL from going into the browser history. The .x in the Redirect.xhtm file name stops the file from being parsed for full-text search info by the HH compiler (hh compiler traditionally only parses files with a trailing .h* ).

Unlike the redirection file in Solution #1 above we never have to edit this file. Also slave topics containing bookmarks are no problem. The following URL works fine:

Tip: Make sure all window types use the $Global_ prefix to force all CHM help into the same window.

Additional - You may not need to use the "its:" protocol prefix. Not needed in the modern IE browser (in fact I've been told the IE7 Beta2 does not like it at all).

 

Solution #3: Mappings on the Application Side, each CHM is a Master

This is a similar to Solution to #2 however instead of using a redirection file we effectively make every slave CHM into a Master CHM. Then it doesn't matter which CHM you open first since they all open and perform the same.

Let's say we have a master.chm (with a master.hhc TOC) and 2 slave chms called SiteA.chm and SiteB.chm.
(Sorry, for this solution I've changed my naming convention from slave1 to SiteA because the downloadable example uses SiteA).

eg. Mapping.ini

[MAIN]
1000=its:master.chm::/index.htm
1001=its:SiteA.chm::/SiteATopic.htm
1002=its:SiteB.chm::/SiteBTopic.htm
...

Notice this time our application mapping file does not need a redirection file. Things are simpler in that respect. The application just grabs the URL from the mapping file and open the help using the HH_DISPLAY_TOPIC API call.

eg. HtmlHelp(0, "c:\SiteB.chm::/Intro.htm>TP", HH_DISPLAY_TOPIC, 0); will show the full merged help.

The only downside to this approach is that HH Favorites information is stored with the CHM name. So if you open Master.chm, setup several favourites links, they will be missing if you then close master and open a slave chm.

 

How do I make all CHMs look the same (effectively all masters)?

Its pretty easy.

  1. Instead of each slave specifying its own hhc TOC file, make then specify the master TOC file instead.
    eg. TP=,"masterCHM::\masterHHC" ,"SiteA.HHK", ....
  2. Each slave should include all other CHMs in its [MERGE FILES] section. That way it also can search and Index across all CHMs (just like a master).

For more detail and a downloadable demo please see

Here's a slight twist you can use (this is used by the code download).

The SiteB.hhp project file contains the following window definition settings

[OPTIONS]
...
Default Window=TP

[WINDOWS]
TP=,"SiteB.HHC","SiteB.HHK","Index_B.htm","Index_B.htm",,,,,0x63520,222,0x304e,[0,0,879,615],0xb0000,,,,,,0
TP2=,"Master.CHM::\Master.HHC","SiteB.HHK","index_b.htm",,,,,,0x1520,,0x0,[271,372,593,566],,,,,,,0

Notice that TP is the default window definition. If you open SiteB.chm normally it will show only its own TOC.
However if you open SiteB.chm using the TP2 window definition then you will see the merged help TOC.

Following this logic you could then make your mapping file as follows.

[MAIN]
1000=its:master.chm::/index.htm>TP2
1001=its:SiteA.chm::/SiteATopic.htm>TP2
1002=its:SiteB.chm::/SiteBTopic.htm>TP2
...

Notice here that whenever a slave CHM is opened, the >TP2 means it is opened using the TP2 window definition and thus you will always see the master (merged) TOC. Note: The URL>WinDefName syntax is standard HTML Help syntax (see HH Workshop online help for more info).

As always it is good practice to prefix your window def names with $Global_ (so everywhere TP or TP2 is defined or used rename to $Global_TP and $Global_TP2). See section on $Global_ for more info. This old example code does not do this but probably should. If you open master, then slave 1, then slave 2 and end up with 3 separate windows instead of 1, then you know you should have used the $Global_ prefix.