Introduction to Customizing the Microsoft Project Guide

This content is no longer actively maintained. It is provided as is, for anyone who may still be using these technologies, with no warranties or claims of accuracy with regard to the most recent product version or service release.

 

Tim Harahan
Microsoft Corporation

June 2002

Applies to:
   Microsoft Project 2002

Summary: This article will show how to extend the Microsoft Project 2002 user interface by customizing the Project Guide. Custom Project Guides are powerful tools that can present Microsoft Project functionality to users in terms of their individual business methods. This article will walk through common steps in the customization process, and explain the basics of the Project Guide's structure in the process. At the end of this article, you should be able to go from modifying existing sidepanes to creating entire custom Project Guides.

**Note  **For an overview of the Project Guide architecture, see Microsoft Project 2002 Project Guide Architecture and Extensibility.

Download Cpgsamples.exe.

Contents

Introduction
Building a Sidepane
What's in a Main Page?
Event Handling
Hosting non-Project Guide pages
Wizards
Persisting the Project Guide's State
Conclusion
Appendix A: Default Main Page Event Handlers
Appendix B: Default Main Page User-Specifiable Events
Appendix C: Deployment Command Reference
Appendix D: Default targetPage Parameters for the LoadWebBrowerControl Command
Appendix E: Selected Functions from the Default Main Page

Introduction

This article assumes you are familiar with Extensible Markup Language (XML), Hypertext Markup Language (HTML), dynamic HTML (DHTML), scripting, and the Microsoft® Project Object Model. Note that the downloadable samples contain hard-coded paths, and must be unzipped to C:\CustomPG\Intro in order to work.

Project management software thinks about business in abstract terms of tasks, resources, and assignments. That's great for customers in general, because it can be adapted to serve any business model. That's not so great for individual users who have to translate their business practices into those abstract terms to make their software relevant. Why should people ask, "What's the latest finish date along this task path?" when they really want to know when the plumber will be done? They shouldn't have to ask that question, which is where the Microsoft Project Guide comes in. It offers a way to extend the Microsoft Project interface to address specific business processes.

Click here for larger image)

Figure 1. Do your users know what they can do with this? (Click to enlarge)

Click here for larger image)

Figure 2. How about this? (Click to enlarge)

The Project Guide gives Microsoft Project its first goal-oriented interface. It puts the Microsoft Project view area into an HTML framework, accompanied by sidepanes that manipulate Microsoft Project through its object model. Organizations can use those sidepanes to present what's happening in Microsoft Project in terms of their own business methods. With the Project Guide, the person customizing the Project Guide is the only person who needs to think of their business in both their organization's terms and in the software's language. Everybody else in the organization gets the benefits of Microsoft Project without needing to learn its language.

This article will show how to customize the Project Guide for a specific business. It will walk through common steps in the customization process, and it will explain the basics of the Project Guide's structure in the process. At the end of this article, you should be able to go from modifying existing sidepanes to creating entire custom Project Guides.

Building a Sidepane

Technically, sidepanes are Microsoft Internet Explorer content. The Project Guide turns everything below the Microsoft Project toolbars into an Internet Explorer display area. The Microsoft Project view appears as an ActiveX® control in the right frame, while sidepane content shows up on the left. The default Project Guide sidepanes are DHTML and Microsoft JScript, though sidepanes can be anything Internet Explorer will host.

Click here for larger image)

Figure 3. Parts of the Project Guide (Click to enlarge)

Users see the Project Guide as a collection of sidepanes, each of which helps them perform some task. Sidepanes whose tasks are related fit into goal areas, forming a navigation hierarchy. Each goal area shows up in the Project Guide as a button on the Project Guide toolbar. Clicking on that button brings up a menu of all the sidepanes in that goal area, where every menu item is a sidepane link. Adding sidepanes to the Project Guide is a three-step process:

  1. Create the sidepane interface as an HTML page.
  2. Bind script to its controls and event handlers to handle any communication with the Microsoft Project object model.
  3. Plug the results into the goal area framework so users can find them.

Let's run a scenario to see exactly how it works. You have just been hired by City Power & Light's Information Technology (IT) department. They're longtime Microsoft Project users who want to start implementing a custom Project Guide. They're tracking a lot of plant construction so they want to spruce up their critical path monitoring. Management wants a Critical Path Monitor sidepane that will let a user click on a critical task, then hit Previous and Next buttons to navigate the critical path. They want the Critical Path Monitor added to the default Project Guide as a first step towards turning the default Project Guide into a City Power & Light Project Guide. What does it take to add that sidepane?

Building a Sidepane: The DHTML

It's not hard to put together the interface for a basic Critical Path Monitor, but there are a few Project Guide-specific details to be dealt with. The complete source is in critpath.htm, included in the downloadable code for this article. Most of it is straightforward DHTML. Here are the Project Guide specific elements, starting with the file header.

<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="MSThemeCompatible" content="Yes">
<title> Microsoft Project </title>
<script src="file:\\c:/CustomPG/Intro/Samples/CritPath/critpath.js" language="JScript"></script>
<script src="gbui://util.js" language="JScript"></script>
<link rel="stylesheet" href="gbui://ProjIE.css" type="text/css" />
</head>

This sets up some standard content and charset data before loading some common Project Guide scripts and styles. The "gbui://" in the src statements refers to files in the Microsoft Project resource DLL, where all the default Project Guide files can be found. The Critical Path Monitor will have some extra scripts of its own, so critpath.js will be included for them. Also note the inclusion of ProjIE.css and util.js, which provide some commonly used Project Guide styles and functionality.

Next, look at the HTML body tag, with its initialization code and event handlers. The handleResize() and pDisplayTaskHeader() calls are standard sidepane functions that help to automate setup tasks. The pSetupSidepane() function, which gets called in the init function, is also part of that process. Those functions, combined with the ProjIE.css styles, keep the new sidepane's look and feel consistent with the default Project Guide. Here's the HTML part of that process, along with notes on what those functions do. For more details, including a look at the init function's code, see the "Building a Sidepane: The Script" section in this article.

<body onLoad="handleResize();init()"
   onUnload="cleanup()"
   onContextMenu="return false" tabIndex=1>

<script language="JScript">
// Must be the first setup call made. Doing pSetupSidepane prior to 
// this can mess up formatting.
pDisplayTaskHeader();
</script>
handleResize Adds, adjusts, or removes scrollbars to accommodate the sidepane's current size. This should be called whenever the sidepane's size changes.
pSetupSidepane Makes any needed goal bar adjustments based on the new sidepane.
pDisplayTaskHeader This formats the title and forward/backward buttons into a neat sidepane header. It needs to be the first formatting call made, so it should be called from a script block in the HTML as shown above.

These functions come from the Project Guide's functional layout page, known as the main page. It's an HTML page whose frameset provides a layout for the Project Guide, with sidepanes in its left frame and the Microsoft Project view in its right frame. There's more detail on main pages in the "Building a Sidepane: The Project Guide Content Schema" section of this article. For now, be aware that a lot of common sidepane layout and navigation scripts are kept in the main page's script files. Main pages are good places to keep common functions, since the main page will always be available to each sidepane.

Moving on, here's the framework for the content portion of the sidepane.

<div id=divMain class="divMain" onResize="handleResize();">
   <div class="SidepaneData">
      <!—Content goes here
   </div>
   </div>

You can put whatever you want into the SidepaneData, but it's a good idea to keep the divMain and SidepaneData styles as they are. It's an easy way to keep the look of your sidepanes consistent with the rest of the Project Guide. Also be sure to call handleResize() for every onResize event, which will insure that the sidepane adds and removes its scrollbars to suit its new size.

Next comes a series of divs and controls containing everything that can ever appear in the sidepane. Most of that will be hidden and revealed as needed, by changing each section's style.hidden property. That's a pretty standard DHTML practice, which works well here.

Also note that the default Project Guide uses tables to mimic buttons instead of working with HTML button controls. That was done to make localization easier, which may not be a factor for individual customers. The table buttons do help maintain a consistent look and feel between default Project Guide buttons and custom ones, so it's still a good idea.

Here's a standard footer for Project Guide sidepanes. The "GoBack" link uses the main page's navigation code to get back to the goal area containing this project. It's best to use main page's navigation code wherever possible to take advantage of its history management. That'll help integrate with the Project Guide's forward and backward navigation buttons.

<!-- This will create a "Done" link at the bottom of the sidepane. It 
       returns the user to the previous point. -->
<p class="rightAlign">
   <a title="Done" id="GoBack" class="trigger" href="#"
      onClick="pNavigate(4,-1,'GoBack')">
      Done
   </a>
</p>
<script language="JScript">
      pDisplayTaskFooter();
</script>
pNavigate Orders the main page to navigate to another Project Guide sidepane. The first parameter is the number of the goal area being navigated to. The second number is the number of the target sidepane in that goal area. -1 tells pNavigate() to go to the goal area's menu. The last parameter is the ID of the element doing the navigation.
pDisplayTaskFooter This orders the sidepane to display any Project Guide footer elements it has, such as a Help link. See the XML Schema section for more details.

That's it for the Guide-specific parts of the sidepane DHTML. You can look at the complete critpath.htm file in the downloadable code for more details. Meanwhile, let's move on to the script.

Building a Sidepane: The Script

Anything you can do with a macro you can do through the Project Guide. However, macros are built with Microsoft Visual Basic® for Applications (VBA), which interprets its input a bit differently than the Internet Explorer script handlers. You'll need to reformat the macro code to make it run. The two keys to keep in mind are the window.external object and the lack of named parameters.

To get at the Microsoft Project object model, you need the window.external object. It provides a reference to the window object associated with the script's HTML document. Since Project Guide scripts ultimately run in Microsoft Project windows, window.external gives access to the Microsoft Project object model. For a reference to the Microsoft Project Application object, use window.external.application. For the current Project object, use window.external.parent. It can be a bit confusing to figure out which project document is active during document loads or context switches, so be careful about using window.external.parent during those operations. Also, don't cache window.external references outside the scope of a function. Those references may end up invalid when the script returns and yields to Microsoft Project.

On the named parameter front, JScript handles optional variables differently than VBA does. Suppose you want to call a function with six parameters, all of which are optional. You want default values for parameters one, two, and three, but not for parameter four. It needs to work like this:

VBA: foo paramFourName:="bar"

JScript: foo(null, null, null, "bar");

JScript figures out which parameter is which by position. Microsoft Project will interpret null values as defaults, which is why parameters five and six aren't specified. The first three parameters need to be there so Microsoft Project knows that "bar" is parameter four. The unspecified values in parameters five and six get converted to defaults inside the object model, just like the values in positions one through three.

Take a look at this function for an example of Project Guide script. It's the update function from critpath.js, which is available in the downloadable code.

//**********************************************************************
// NAME: update
// PURPOSE: This function gets called whenever the user's selection 
// changes. It will test to see if the new selection is a critical 
// task. If so, it displays the critical path navigation buttons. If 
// not, it displays the 'Please select a critical task' sidepane. 
// Either way, this function will also update the displayed project 
// start and finish dates.
// PARAMETERS: 
// sel — Pointer to the Selection object representing the new selection.
// selType — 0 for a task selection, 1 for a resource.
// RETURN: None
//**********************************************************************

function update(sel, selType)
{
   try
   {
      // Cache references to the object model's current Project and 
      // Application objects.
      var application = window.external.application;
      var thisProject = window.external.parent;

      // Use the object model text converter to format the dates in a 
      // localization friendly way.
      startDate.innerText = 
         application.DateFormat(thisProject.ProjectStart, 1);
      finishDate.innerText = 
      application.DateFormat(thisProject.ProjectFinish, 1);

      // Init a Bool to track whether the selected task is critical. 
      // Assume it isn't.
      var isCritical = false;

      try
      {
         // See if the selected task is critical. This
         // isn't great critical task coding because it won't handle 
         // multiple task selections well. It's enough for a basic
         // Project Guide demonstration, though..
      isCritical = sel.Tasks.Item(1).Critical;
     }
      catch(Exception)
      {
          // Leave the isCritical flag false if the selection had no
          // associated task.
      }

      // Show and hide the appropriate pieces of the display.
      if(true == isCritical)
       {
         criticalInfo.style.display = "";
         nonCriticalInfo.style.display = "none";
      }
      else
      {
         criticalInfo.style.display = "none";
         nonCriticalInfo.style.display = "";
      }
   }
    catch(Exception)
   {
      // Suppress any excess script errors to avoid user confusion.
   }
}

Scripts can also tap the main page for easy event handling. The main page already catches several common events, including the SelectionChanged event you need to handle. Every time a new task gets selected, it might or might not be critical, so the sidepane needs to update accordingly. The Critical Path Monitor needs to register its update function with the main page's SelectionChanged handler on load, and it needs to unregister its update function when it unloads. Take a look at the crithpath.js init and cleanup code to see how it's done.

Note that the Project Guide doesn't work well when the Microsoft Project events have multiple listeners. What if the first listener to receive an event cancels it so that your sidepane never picks it up? Consolidating sidepane event handlers into one main page-based receiver can help avoid that scenario.

//**********************************************************************
// NAME: init
// PURPOSE: Here we set up the sidepane and register update() as the 
// selectionChanged event handler. This gets called when the sidepane 
// first loads.
// PARAMETERS: None
// RETURN: None
//**********************************************************************

function init()
{
   try
   {
      pSetupSidepane(true); 

      // Cache a reference to the main page script object.
      var script = pscript();

       // Set update as the current recipient of selectionChanged events.
      // All the setHandler functions take a function name as a 
       // parameter, making the main page call that function when the 
       // correct event comes in.
      script.setSelectionHandler(update);

       // Call update to init the sidepane, then use applyCriticalStyle 
       // to highlight the critical path in the Microsoft Project view.
       update(null, null);
       applyCriticalStyle();
   }
   catch(Exception)
   {
      // Suppress any excess script errors to avoid user confusion.
   }
}

//**********************************************************************
// NAME: cleanup
// PURPOSE: This will unregister update as the current Project Guide 
// selectionChanged event handler.
// PARAMETERS: None
// RETURN: None
//**********************************************************************

function cleanup()
{
   try
   {
       pscript().SetSelectionHandler(null);
   }
   catch(Exception)
   {
      // Suppress any excess script errors to avoid user confusion.
   }
}

This way, there's only one registered event listener up on the main page. That listener does nothing but pass the event on to whatever function is currently set as its event handler. SetSelectionHandler() only sets the handler for the SelectionChanged event. Look at the "Event Handling" section of this article for more details on event handling in the default main page. The rest of the script is straightforward, so let's move on to the last step: fitting the sidepane into a goal area.

Building a Sidepane: The Project Guide Content Schema

Take a look at the organization of the default Project Guide.

Click here for larger image)

Figure 4. Goal areas and goal area tasks of the default Project Guide. Note that this is not a complete list of all goal area tasks. (Click to enlarge)

Each sidepane fits into a goal area, named for the goals those sidepanes work towards. Task related sidepanes like "Schedule tasks" fit into the Tasks goal area, report related sidepanes like "See project costs" in the Report goal area, and so on. Users can find the functionality they need by looking at the goal area they want to work with and running down the list of options. It's a good system with a very simple XML skeleton. GoalArea nodes define goal areas, and GoalAreaTask nodes define single sidepanes. Take a look at this excerpt from sample.xml, in the downloadable code section.

<GoalArea>
   <GoalAreaID> 4 </GoalAreaID>
   <GoalAreaName> Report </GoalAreaName>
   <GoalAreaDescription>View and report the status of your projects by clicking an item below. Clicking an item displays tools 
and instructions for completing that step. </GoalAreaDescription>
   <URL> gbui://report_main.htm </URL>
   <RelevantViews>
      <ViewType> 0 </ViewType>
      <ViewType> 1 </ViewType>
   </RelevantViews>
   <!—GoalAreaTasks go here
</GoalArea>

This is the definition of one goal area. As we saw before, the GoalAreaID is an identifier used in navigating to sidepanes from this goal area. GoalAreaIDs need to be unique to accommodate the navigation code, but they don't need to be in any particular order to work. GoalAreaName controls how this goal area will be listed on the Project Guide toolbar. The URL node gives the URL of the goal area's menu sidepane. The default main page creates goal area menus based on the content schema, so there's no need to modify the goal area's HTML when you're adding a sidepane. Just put the new GoalAreaTask in a GoalArea node, and the main page will parse it out and add it to the goal area menu. For an example, take a look at the Critical Path Monitor's GoalAreaTask. You can find it in the Reports goal area (GoalID 4) of the sample.xml file in the downloadable code.

<GoalAreaTask>
   <TaskID> 34 </TaskID>
   <Title> Critical path monitor </Title>
   <TaskName> Monitor the critical path </TaskName>
   <URL> file:\\c:/CustomPG/Intro/Samples/CritPath/critpath.htm </URL>
   <TaskHelp>
      <HelpName> More Information </HelpName>
      <URL> file:\\c:/CustomPG/Intro/Samples/CritPath/critpath_help.htm </URL>
   </TaskHelp>
   <RelevantViews>
      <ViewName> Gantt Chart </ViewName>
   </RelevantViews></GoalAreaTask>

The Title will appear in the sidepane's header, and the TaskName will be its entry in the Reports goal area menu. URL gives its file location. TaskID is a unique number used to identify the task in navigations. TaskHelp is an optional section. The pDisplayTaskFooter function in the HTML uses TaskHelp to create a footer with a link to the sidepane's Help file. HelpName controls the link's label while URL controls its destination.

The other thing to do in the schema is to determine relevant views. Some sidepanes only make sense when the Microsoft Project view control has particular views showing. Suppose you've got a sidepane about estimating task durations. If a user puts a resource view next to that sidepane, it won't make sense anymore. By default, the main page gets out of that situation by getting rid of the sidepane. The main page will respond to any non-relevant view change by closing the current sidepane and going back to its goal area menu. How does it tell relevant views from non-relevant ones? The RelevantViews section of the GoalAreaTask XML node specifies which views are relevant.

Any GoalArea or GoalAreaTask node can have a RelevantViews section to declare its relevant views. Note that view checks bubble up. If a GoalAreaTask has no RelevantViews node, then the GoalAreaRelevantViews will be applied to it instead. RelevantViews nodes can contain one of three types of view declaration nodes, each of which specifies things a bit differently. ViewType nodes declare all task or resource views relevant. <ViewType>0</ViewType> means task views, 1 means resource views, and -1 means that no view is relevant. ViewType -1 should be used on any GoalAreaTask without relevant views to insure that the GoalArea relevant views aren't applied to the GoalAreaTask. ViewScreen and ViewName specify individual views. ViewScreen nodes contain numbers taken from the object model's pjViewTypeEnum type. ViewName contains the view name. Examples of all these are shown in the sample.xml code sample accompanying this article.

The position of GoalAreaTask nodes also matters. Sidepane tasks show up on the goal area menu in the order of their listings in the GoalArea node. The first GoalAreaTask in a GoalArea will be the first thing on the menu, the second GoalAreaTask will be second on the menu, and so on. Compare the GoalAreaTask order in sample.xml to the onscreen goal area menus for examples.

Now you've got an XML file with its new Critical Path Monitor GoalAreaTask in the Reports goal area. All the sidepane code is in place. Time to turn it on.

Building a Sidepane: Switching to Custom Content

Get critpath.htm, critpath.js, and sample.xml from the downloadable code sample, cpgsamples.exe, available at the beginning of this article. Those files contain everything that's been walked through here. Start Microsoft Project and go to the Interface tab of the Options dialog box (Tools menu) on the Microsoft Project toolbar. Near the bottom is a section marked Project Guide Content. Switch it to Use custom content and enter the path to sample.xml in the dialog box that appears. Once you click OK, the Project Guide should refresh and show the Critical Path Monitor in its Report goal area. Now you're online with a custom Microsoft Project Guide. Go to the bottom of the Report goal area menu and you'll see the Critical Path Monitor.

Do note that the Project Guide refreshes its schema only when it sees the schema's name change. If you go back later and add another sidepane to sample.xml, you'll need to turn the Project Guide off and on again to do a refresh. Toggling the Project Guide on and off will always refresh the toolbar and goal area menus, so keep that in mind if the Project Guide ever gets out of sync with the schema.

If you're prototyping a Project Guide or deploying to a single user, then you're done. If you're rolling the custom Project Guide out for a big organization, then deployment will take a bit more work. Deploying a custom Project Guide means changing its content schema or main page to a custom version, which makes the custom sidepanes or layout available to the user. There are two challenges to doing that. First, you need to get the custom files out to users. That's not too hard. You can ship the files out to user machines, or you can store the files centrally on a network share or server. It's your call, based on whether you'd rather deal with the challenge of maintaining one key server or updating every user machine.

The second challenge is figuring out how to get every copy of Microsoft Project in your organization to use the content once it's accessible. You've got two options for that. First, you can change the default Project Guide settings stored in the registry via system policies. That comes in handy when rolling out to large user bases, especially if you want to keep end users from changing the new settings. The Microsoft Project 2002 Resource Kit has details on how to use system policies with Microsoft Project settings.

The simpler option is to change the content schema or main page through the Microsoft Project object model. Take a look at Appendix C for a list of the object model commands needed to script those schema and main page changes. Once you have a script that will switch a user over to the custom Project Guide, it's up to you to get that out to your users. Microsoft Project templates containing the script are one good distribution method. Auto-open macros also work, and are great for attaching custom Project Guides to individual Microsoft Project files. Some auto-open macros can examine the project being opened, figure out the user's role in that project, and switch to a role-specific Project Guide on the fly. There are plenty of options for deploying a custom Project Guide, regardless of what your customization is and what kind of user base it's going out to.

And that's that! You just built a sidepane, plugged it into a Project Guide, and rolled it out. Aside from a couple of script calls to work with the main page framework, customizing the Microsoft Project Guide is a matter of Web design. Once you know the Microsoft Project object model, you can format your functionality however you choose, in whatever terms your customers need. The main page framework will take care of the details of navigation and event handling so that you can focus on tailoring the Project Guide to your customer's needs. New sidepanes and goal areas can be added to the Project Guide in minutes by changing small pieces of the Project Guide's content schema. Try making your own custom sidepanes using the blank_template.htm file in the downloadable code and you'll see how simple sidepane work is.

What's in a Main Page?

Individual sidepanes fit into a framework defined by the Project Guide's functional layout page. When the Project Guide turns on, Microsoft Project 2002 replaces its document window with an Internet Explorer window, which contains the functional layout page. The functional layout page, also known as the main page, has one job: provide a layout for the contents of that Internet Explorer window. Main pages often take on extra jobs; since they're good places to put commonly used sidepane support code. Still, the only essential job is to provide a layout, usually by defining it as an HTML frameset.

You can make any HTML page a main page in three steps. First, make it either local or trusted. Note that the Project Guide doesn't have its own trusted sites list, so it will trust anything that Internet Explorer trusts. Second, mark it as a main page by putting the comment "<!-- ProjectGuideMainpage -->" at the start of the file. Microsoft Project won't accept any main page without that comment. Third, select Use custom functional layout page from the Interface tab in the Options dialog box (Tools menu) on the Microsoft Project toolbar. A text box will appear with the current main page's path name in it. Replace that with your main page's information and hit OK.

The default main page plays three other roles besides layout provider:

  1. Catch events from Microsoft Project and handle them accordingly. This includes catching view change events and using them to enforce relevant views. See the "Event Handling" section of this article for event details and the "Building a Sidepane: The Project Guide Content Schema" section for more information on relevant views.

  2. Persist the Project Guide state using the Project.ProjectGuideSaveBuffer property. See the "Persisting the Project Guide's State" section of this paper for details.

  3. Translate the content schema into a Project Guide structure and manage navigation through that structure. Look at the "Building a Sidepane: The Project Guide Content Schema" section of this article for details.

    These three jobs are done through the main page just to centralize their code, since they're common operations. Check the mainpage.* files in the downloadable code for the implementations. For more in-depth information on any of these areas, check the Microsoft Project 2002 Software Development Kit (SDK) or the attached reference code. The SDK will be available soon on the Microsoft Project Developer Center on MSDN®, the Microsoft Developers Network.

Event Handling

Any main page that wants to catch Microsoft Project events needs to include the following code:

<!-- Document events handlers -->
<object id="MSPJDocObj"
       ALT="Microsoft Project Document Event Handler"
       classid="CLSID:494B3458-3EFF-4C66-9C86-D47670D69634"
       style="display:none">
</object>

<!-- Application events handlers -->
<object id="MSPJAppObj"
       ALT="Microsoft Project Application Event Handler"
       classid="CLSID:04EEB710-3EB7-4B69-9281-E9BBB7B35959"
       style="display:none">
</object>  

These objects allow the main page to handle application and project level events thrown by Microsoft Project through standard script event handlers. The default main page catches several common events. Mainpage.htm rarely hardwires the script in charge of the events so that the sidepanes can get a chance to handle the events. Instead, it keeps a variable around in its script that can be accessed using a setEventHandler function. This way, individual sidepanes can share that one main page event listener. There should only be one listener in the Project Guide for any given event, to avoid confusion over fallouts from that event. What if a listener acted on an event, then cancelled it? The other listeners would never know it happened.

There's a list of events handled by the default main page in Appendices A and B following this article. If you want to handle an event that's not on the list, you'll need to do more than just call a setEventHandler function. Let's run a case study. Suppose you've been asked to build a sidepane that displays data on newly created tasks. The way to do it is through the Application.ProjectTaskNew event, which isn't on the default list. You need to add support for that event to the default main page. It's a four-step process, shown below:

  1. Catch the event. You can do that by inserting the following code somewhere after the MSPJAppObj object in mainpage.htm.

    <script for="MSPJAppObj" event="ProjectTaskNew(pj, ID)" 
       language="JScript">
    try
    {
       // Here, we're checking to see that the project with the new 
       // task is the current project. When users load documents or 
       // switch between projects, it's possible for one document to 
       // get another document's events,so this is a necessary check.
       // You can still do this test for events that pass a Window 
       // reference rather than a Project. In that case, compare the
       // Window.Index property to the current document's window index.
       // Look to the LoadWebPage event handler in mainpage.htm
       // for an example.
       if(pj == window.external.Parent)
       {
          handle_AfterTaskNew(pj, ID);
       }
    }
    catch(exp) {}
    </script>
    
  2. Declare the sidepaneHandlerAfterTaskNew variable in mainpage.js. There's a section of mainpage.js labeled "window event handlers" for application event handler declarations, and another called "document event handlers" for document level ones. It's not critical to declare the variable in those sections, but it is a useful coding convention.

  3. Add the handle_AfterTaskNew function to mainpage.js. This function just needs to be able to accept handler functions from the sidepane, so it'll look like this:

    function handle_AfterTaskNew(pj, ID)
    {
       var eventHandled = false;
       if (null != sidepaneHandleAfterTaskNew)
       {
          // The mainpage's sidepaneHandlerAfterTaskNew handler will 
          // see if any sidepane handlers are registered for this 
          // event. If so, it'll call the sidepane's function will 
          // the event parameters. If not, it'll do whatever default
          // handling it needs to.
          eventHandled = sidepaneHandleAfterTaskNew (pj, ID);
       }
       if (false == eventHandled)
       {
          // If the mainpage wanted to do any default handling, it would 
          // do it here. Right now there's no need, so this section is 
          // blank.
       }
    }
    
  4. Create a setEventHandler function so the new sidepane can register its handler as the AfterTaskNew handler.

    function setAfterTaskNewHandler(newSelectionHandler)
    {
       sidepaneHandleAfterTaskNew = newSelectionHandler;
    }
    

The main page has no way to know whether an event handler should stay around after its sidepane is unloaded. It's a good idea for each sidepane to reset its event handlers when it unloads. Just call the appropriate setHandler function with null as the input, rather than a function name.

The ViewChange event does a bit more work to handle relevant views. As described in the "Building a Sidepane: The Project Guide Content Schema" section of this paper, the default main page will check for view changes. If the new view isn't on the current sidepane's relevant views list, then the main page will navigate the sidepane back to the goal area's menu page. Sometimes that default view handling gets in the way, which is what the blockNextViewChangeHandling function is for. Every blockNextViewChangeHandling call makes the main page ignore one subsequent ViewChange event. That will let you to suppress view change handling if it's getting in the way.

Overall, event handling is simple. There should only be one listener for any given event, so the listeners themselves stay on the main page. Each sidepane that wants an event orders the main page to call its function as that event's handler. When the sidepane unloads, it orders the main page to use null as the event handler, which tells main page to go back to its default handling. Any sidepane can handle any Microsoft Project event in this way.

Hosting non-Project Guide pages

At heart, the Project Guide isn't sidepanes or scripts. It's Internet Explorer. The Project Guide orders Microsoft Project to replace its document window with an Internet Explorer window. The Project Guide goes on to put its main page and sidepane in that Internet Explorer window, but that doesn't have to be the case. The Internet Explorer control itself is independent of the Project Guide, so it can host any local or trusted content. That's where Application.LoadWebBrowserControl comes in.

The LoadWebBrowserControl function can bring up Internet Explorer content that's not in the Project Guide content schema. It makes certain that Microsoft Project has an Internet Explorer window and a main page up, then sends that main page an Application.LoadWebPage event with orders to load a target Internet Explorer page. When the Project Guide is running, there's already an Internet Explorer window available, so LoadWebBrowser just sends the LoadWebPage to the Project Guide main page. If the Project Guide is off, then LoadWebBrowserControl brings up the Internet Explorer window itself and uses its wrapperPage parameter as the window's main page.

LoadWebBrowserControl doesn't actually navigate the Internet Explorer window to the target page. That's left to the window's main page, regardless of whether that's the Project Guide main page or a LoadWebBrowserControl*wrapperPage.*LoadWebBrowserControl passes its targetPage parameter on to the main page in a LoadWebPage event. The main page is responsible for translating that parameter into a URL and ordering the navigation. This means that any main page needs to handle LoadWebPage, whether it's meant for use by the Project Guide or as a wrapperPage. This also means that a given user may have several different LoadWebPage handlers floating around, each of which has to interpret targetPage values consistently. The default Project Guide solves the problem by using numbers as targetPage parameters, then translating those numbers into URLs in mainpage.js and wrapper.js. That eliminates the need for URL validation on targetPage values. See Appendix D for a list of wrapper.js's targetPage to URL translations.

None of this should keep you from navigating the right pane directly to the target URL when the Project Guide is on. That's how the Project Guide brings up Microsoft Server pages in the Add Documents sidepane. Note that you must have Microsoft Project Server set up before you'll be able to see that yourself.

When you're finished with the Web page, call Application.UnloadWebBrowserControl to clean it up. UnloadWebBrowserControl will always navigate back to the view that was up before LoadWebBrowserControl got called. If the Project Guide is off, it will also take out the Internet Explorer window and replace it with a Microsoft Project document window. It's useful to wire an UnloadWebBrowserControl call to a button in the LoadWebBrowserControlwrapperPage, so that users can readily see how to get back to their regular view.

Integrating the Internet Explorer Web browser control into the Microsoft Project has a few other side effects. Microsoft Project lets Internet Explorer handle cut, paste, copy, and print operations in the Internet Explorer window. If a user puts the focus in the Microsoft Project view control before a copy or print, they'll see what they'd expect from traditional Microsoft Project user interface (UI). Printing a screen of sidepanes alongside the view control may not work, since that's a function of Internet Explorer frame printing. This shouldn't be a problem, but be aware that Internet Explorer is in charge of Web browser copy, paste, and print work.

In the end, LoadWebBrowserControl will let you put any trusted or local Web page into the Microsoft Project UI. UnloadWebBrowserControl lets you clean that page up when the user is done with it. These two object model commands will work whether the Project Guide is turned on or off. They're especially useful when integrating Web services like Microsoft Project Server into the Microsoft Project desktop.

Wizards

Some business tasks don't fit neatly into a single sidepane. Plenty of jobs involve a lot of steps that only make sense when taken together. Think about what's involved in setting up tracking for a project. You need to decide what tracking method to use, whether you want the Microsoft Project Server to help, how you want progress to be reported, and so on. It's too much for a single sidepane, but fits well into a wizard.

Click here for larger image)

Figure 5. A basic wizard sidepane (Click to enlarge)

Wizards are just sidepanes with multiple steps. The user sees a wizard as a collection of sidepanes linked by a common navigation footer and a set of wizard UI conventions. When a user is in a wizard, the sidepane header's forward and back buttons will navigate between steps of the wizard rather than jumping straight out of the sidepane. Each step of the wizard gets treated as its own page as far as that forward/back history is concerned. Also, when the user tries to leave the wizard by any means other than hitting its Save and Finish button, they'll get a dialog box asking them to confirm their exit. If they hit Cancel, they stay where they were in the wizard. This adds up to a user experience a lot like any Microsoft wizard, where the user is being walked through a complex task step-by-step with a safety rail to discourage leaving it too early.

The default main page makes this simple. It has code to generate the footer, to handle wizard mode navigation, and to deal with the alerts. SetWizardModeOn() and setOnMoveToStepHandler() are the keys to using that. The OnMoveToStep handler is the real heart of a wizard. Each wizard step's HTML sits in a separate div, so step navigation is a matter of hiding the current step's div while revealing the new step. The default main page thinks of navigation as going from URL to URL so it can't handle this. Instead, when wizard mode turns on, the main page starts passing step navigations to an onMoveToStep handler. Each wizard supplies its own onMoveToStep handler by calling setOnMoveToStepHandler(). An onMoveToStep function takes one parameter, the number of the next step to be shown. If that next step input is less than 1 or greater than the total number of steps in the wizard, then it's a signal to exit the wizard.

Wizard mode does a few other things. It wires the forward and backward buttons to the onMoveToStep handler so they can do step navigations. It also turns on the wizard's exit confirmation dialog. Finally, it makes the main page respond to the getWizardCurrentStep and setWizardCurrentStep calls. The onMoveToStep function should keep the main page in sync with its current step using setWizardCurrentStep after each navigation.

On the HTML side, wizards differ from ordinary sidepanes in two ways. First, they need to have their steps in separate divs for easy step navigation. Second, they need to generate standard wizard footers. The default main page's write_WizardFooter function makes that easy. Each call to write_WizardFooter generates one footer with a previous step link, a next step link, and a "Step X of Y" style caption. The footer is a div with "StepXofY" as its ID, which makes it easy for the onMoveToStep function to hide and reveal footers during step navigation. Note that you can create multiple footers for a single step number, such as "Step 2 of 3" and "Step 2 of 4". That helps a lot if you're building a wizard whose step count varies, like the default Project Guide's Define New Project wizard.

Wizard templates are provided in the attached code. A look at those will show you the essentials of coding wizards, both in HTML and script. Take a look at the initWizardScripts, cleanupWizardScripts, and moveToWizardStep functions to see how it works.

Persisting the Project Guide's State

When a user opens a Microsoft Project document, they'll see the same Project Guide sidepane they saw when they closed the project. That's persistence. The Project Guide stashes an XML schema of save data in the object model's Project.ProjectGuideSaveBuffer. The default main page uses loadSavedSidepaneProperties() and loadSavedStartPage() to load the saved state, saveState() to save it out, and createSaveXML() to format the data to be saved. It's straightforward XML manipulation, using the ProjectGuideSaveBuffer to persist the XML between sessions. Take a look at this save state XML, drawn from the default main page.

<GBUIBuffer Version="1.00">
   <SidepanePersistanceInfo SidepaneDisplayState="open">
      <SidepaneWidth>200</SidepaneWidth>
      <GoalAreaID>1</GoalAreaID>
      <TaskID>10</TaskID>
   </SidepanePersistanceInfo>
</GBUIBuffer>

It persists the current sidepane width, goal area, and TaskID within that goal area. The above save schema tells the Project Guide to navigate to the Task goal area's Publish Project Information to Web sidepane on load. The goal area and TaskID are drawn from the content schema of the Project Guide that generated them. Note that this may cause unexpected results if a file is saved under one content schema and loaded under a different content schema. This won't cause crashes, but will likely bring up the wrong sidepane on load.

You can customize the XML save structure however you want as long as you respect two rules. First, only add content to the GBUIBuffer node. If that's not the root, the default main page will reject the save schema. Second, don't change anything inside the SidepanePersistanceInfo node. The SidepanePersistanceInfo tag name is reserved for default save data. The ProjectGuideSaveBuffer can store up to 64K of data, so there's plenty of room for expansion.

Do note that the save data doesn't necessarily have to be XML. The ProjectGuideSaveBuffer is just a buffer, which can accommodate any type of data you need. The ProjectGuideSaveBuffer stores XML by default only because the default main page uses XML for it's save functionality. If you're willing to live without the main page's save state support, feel free to use your own format for ProjectGuideSaveBuffer data.

Conclusion

The Project Guide gives solution providers a key tool towards overcoming some of the past usability issues in project management software. It can extend the Microsoft Project UI, filling in blanks and making features more accessible. It can also reframe the Microsoft Project functionality entirely, presenting business tasks in business terms through custom sidepanes. The Project Guide is a simple framework of DHTML and script, with an XML schema controlling how its sidepanes are organized. The Microsoft Project object model lets each sidepane act as a fully functional UI, using the Microsoft Project object model to work with both Microsoft Project and Microsoft Project Server. Overall, the Project Guide is a valuable toolkit that any customer can apply to their organization. Good luck with it!

Appendix A: Default Main Page Event Handlers

These events are caught by the main page as part of its default functionality. In the default main page, sidepanes don't get to change these handlers.

Event Purpose
Application.WindowGoalAreaChange Navigates the sidepane to a new goal area menu and updates the toolbar's related activities to match.
Application.WindowSidepaneDisplayChange Updates the Project Guide in response to its sidepanes being hidden or revealed, as by the toolbar's Show/Hide toggle.
Application.WindowSidepaneTaskChange Navigates the sidepane to a new task and updates the toolbar's related activities to match.
Application.WindowActivate Prepares the Project Guide toolbar for action.
Application.WindowDeactivate Prepares the Project Guide toolbar for deactivation.
Application.WorkpaneDisplayChange Hides the sidepane when the workpane appears and vice versa.
Application.LoadWebPage Translates the LoadWebPagetargetPage parameter into a URL, then navigates the right pane there.

Appendix B: Default Main Page User-Specifiable Events

Users can specify handlers for these events using the appropriate setEventHandler call.

Event setEventHandler function
Application.WindowSelectionChange setSelectionHandler(newHandler)
Application.WindowBeforeViewChange setBeforeViewChangeHandler(newHandler)
Application.WindowViewChange setViewChangeHandler(newHandler)
Application.ProjectBeforeSave setBeforeSaveHandler(newHandler)
Application.ProjectAfterSave setAfterSaveHandler(newHandler)
Document.Open setDocumentAfterOpenHandler(newHandler)
Document.BeforeClose setDocumentBeforeCloseHandler(newHandler)
Document.BeforeSave setDocumentBeforeSaveHandler(newHandler)
Document.BeforePrint setDocumentBeforePrintHandler(newHandler)
Document.Calculate setDocumentAfterCalculateHandler(newHandler)
Document.Change setDocumentAfterChangeHandler(newHandler)
Document.Activate setDocumentAfterActivatehandler(newHandler)
Document.Deactivate setDocumentAfterDeactivateHandler(newHandler)

Appendix C: Deployment Command Reference

The Microsoft Project 2002 object model contains new elements to change the Project Guide's main page and content schema, which comes in handy for custom Project Guide deployment. There are four properties to control those settings, and a function that can change them all at once.

Project.ProjectGuideUseDefaultContent

This Boolean controls whether the Project.ProjectGuideContent string can be changed to set a new content schema. If it's true, Project.ProjectGuideContent will always contain the default schema's path. If false, users can change Project.ProjectGuideContent.

Project.ProjectGuideContent

This string contains the full name and path of the Project Guide's content schema. When Project.ProjectGuideUseDefaultContent is false, users can change this to set a new schema. The Project Guide will refresh whenever this property changes to stay in sync with the new schema.

Project.ProjectGuideUseDefaultFunctionalLayoutPage

This Boolean controls whether the Project.ProjectGuideFunctionalLayoutPage string can be changed to set a new main page. If it's true, Project.ProjectGuideFunctionalLayoutPage will always contain the default main page's path. If this is false, users can change Project.ProjectGuideFunctionalLayoutPage.

Project.ProjectGuideFunctionalLayoutPage

This string contains the full name and path of the Project Guide's main page. When Project.ProjectGuideUseDefaultFunctionalLayoutPage is false, users can change this to set a new main page. The Project Guide will refresh whenever this property changes to stay in sync with the new main page.

Application.OptionsInterface([ShowResourceAssignmentIndicators], [ShowEditToStartFinishDates], [ShowEditsToWorkUnitsDurationIndicators], [ShowDeletionInNameColumn], [DisplayProjectGuide], [ProjectGuideUseDefaultFunctionalLayoutPage], [ProjectGuideFunctionalLayoutPage], [ProjectGuideUseDefaultContent], [ProjectGuideContent], [SetAsDefaults])

This command can update any of the settings found on the Interface tab of the Options dialog box (Tools menu), including the Project Guide properties. The DisplayProjectGuide Boolean parameter may also be useful since it toggles the Project Guide on and off.

Appendix D: Default targetPage Parameters for the LoadWebBrowerControl Command

The wrapper.htm page gets used as the default main page for LoadWebBrowserControl calls. One of its jobs is to convert targetPage parameters from LoadWebBrowserControl into URLs, which are used as destinations for LoadWebBrowserControl's navigation. In the default wrapper.htm page, all the targetPage values translate into Microsoft Project Server pages. The following table lists those translations:

targetPage Value Project Server Page
1 Updates page
2 Document Library page for the active project
3 Issues page for the active project
4 Project Center page
5 Resource Center page
6 Portfolio Analyzer
7 Portfolio Modeler

Appendix E: Selected Functions from the Default Main Page

The following list includes some commonly used functions from the default main page. Any sidepane can call these functions through a reference to the script object of their parent main page. See the attached code samples for examples.

This is only an excerpt from the main page's function roster, so refer to the Microsoft Project 2002 SDK or read the attached main page code for a more complete listing. Note that some of these functions come from util.js, a supplement to the main page.

handleResize()

PURPOSE: Ensures that the sidepane correctly adjusts its scrollbars to the size of its content. Call this whenever you hide divs, change displayed text, or do anything else that could change the space needed by the sidepane's content.

PARAMETERS: None.

RETURNS: Nothing.

pscript()

PURPOSE: Sidepanes use this to get a reference to the main page's script object.

PARAMETERS: None.

RETURNS: A reference to the parent main page's script object.

pobject()

PURPOSE: Sidepanes use this to get a reference to the main page HTML document.

PARAMETERS: None.

RETURNS: A reference to the parent main page's HTML document object.

lscript()

PURPOSE: Gets a reference to the left pane's script object. Custom right pane code can use this to get at a sidepane's script.

PARAMETERS: None.

RETURNS: A reference to the left pane's script object.

lobject()

PURPOSE: Gets a reference to the left pane's script object. Custom right pane code can use it to get references to a sidepane's HTML object.

PARAMETERS: None.

RETURNS: A reference to the left pane's HTML document object.

rscript()

PURPOSE: Gets a reference to the right pane's script object. Sidepane code can use this to get at an HTML right pane's script.

PARAMETERS: None.

RETURNS: A reference to the left pane's script object.

robject()

PURPOSE: Gets a reference to the right pane's script object. Sidepane code can use it to get references to the right pane's HTML document object.

PARAMETERS: None.

RETURNS: A reference to the left pane's HTML document object.

pGoBack()

PURPOSE: Orders the main page to navigate one step backwards. In wizard mode, that means ordering the wizard to go back one step. Otherwise, it goes one step back in the main page's history, which usually jumps to the previous sidepane. You should always use location.replace to do any navigations that won't change the current sidepane. That way parent's history only contains navigations from sidepane to sidepane or wizard step to wizard step, which is what users expect from the forward and back buttons.

PARAMETERS: None.

RETURNS: Nothing.

pGoForward()

PURPOSE: Orders the main page to navigate one step forward. In wizard mode, that means ordering the wizard to go forward one step. Otherwise, it goes one step forward in the main page's history, which usually jumps to the next sidepane. You should always use location.replace to do any navigations that won't change the current sidepane. That way parent's history only contains navigations from sidepane to sidepane or wizard step to wizard step, which is what users expect from the forward and back buttons.

PARAMETERS: None.

RETURNS: Nothing.

pClosePane()

PURPOSE: Orders the parent to close the sidepane.

PARAMETERS: None.

RETURNS: Nothing.

pChangeBackLit()

PURPOSE: Replaces the sidepane header's back arrow icon with a lit-up, highlighted version of itself. This is called whenever the mouse enters the back arrow.

PARAMETERS: None.

RETURNS: Nothing.

pChangeBack()

PURPOSE: Resets the sidepane header's back arrow icon to its unhighlighted state. This is called whenever the mouse leaves the back arrow.

PARAMETERS: None.

RETURNS: Nothing.

pChangeForwardLit()

PURPOSE: Replaces the sidepane header's forward arrow icon with a lit-up, highlighted arrow. This is called whenever the mouse enters the forward arrow.

PARAMETERS: None.

RETURNS: Nothing.

pChangeForward()

PURPOSE: Resets the sidepane header's forward arrow icon to its unhighlighted state. This is called whenever the mouse leaves the forward arrow.

PARAMETERS: None.

RETURNS: Nothing.

pChangeCloseLit()

PURPOSE: Replaces the sidepane header's close button icon with a lit-up, highlighted version of itself. This is called whenever the mouse enters the close button.

PARAMETERS: None.

RETURNS: Nothing.

pChangeClose()

PURPOSE: Resets the sidepane header's close button to its unhighlighted state. This is called whenever the mouse leaves the close button.

PARAMETERS: None.

RETURNS: Nothing.

pDisplayGoalAreaHeader()

PURPOSE: Gets the current goal area sidepane's header text from the content schema, then uses write_header to generate a header for it.

PARAMETERS: None.

RETURNS: Nothing.

pDisplayGoalAreaTasks()

PURPOSE: Populates a goal area menu sidepane with task sidepane links. The link data is drawn from the content schema, and the goal area is drawn from the main page activeGoalAreaId variable.

PARAMETERS: None.

RETURNS: Nothing.

pDisplayGoalAreaFooter()

PURPOSE: Goal areas don't currently have a footer, so this is a dummy procedure. It does nothing, but is a useful starting point for anyone wanting to add standard footers to goal area menus.

PARAMETERS: None.

RETURNS: Nothing.

pDisplayTaskFooter()

PURPOSE: Uses write_footer to add a Help link footer to the current sidepane; uses the main page's cached activeGoalAreaID and activeTaskID variables to tell which sidepane is currently loaded.

PARAMETERS: None.

RETURNS: Nothing.

pDisplayTaskHelpHeader()

PURPOSE: Creates a header for the current Help sidepane; shouldn't be called for regular non-Help sidepanes.

PARAMETERS: None.

RETURNS: Nothing.

pSetupSidepane(fSetupGoalBar, fBlockSplitCheck)

PURPOSE: Will sync the goal bar to the current state if ordered to. Can also remove the window split from the Microsoft Project view control.

PARAMETERS: Boolean fSetupGoalBar — When true, setupGoalBar will be used to update the goal bar.

Boolean fBlockSplitCheck — When false, the window split will be removed. Has no effect if the window isn't split.

RETURNS: Nothing.

pNavigate(goalAreaID, taskID, elementID)

PURPOSE: Main navigation function for the default main page. Matches the input IDs to a sidepane's entry in the XML content schema, then brings up that sidepane.

PARAMETERS: Number goalAreaID — ID of the goal area containing the destination sidepane.

Number taskID — Sidepane ID as found in the TaskID field of its XML node. If this parameter is -1, it will navigate to the goal area's menu instead.

Number elementID — ID of the HTML element launching the navigation. If the navigation was launched by script and not in response to a user clicking a navigation link or button, then set this to null.

RETURNS: Nothing.

pWizardMoveToStep(stepID)

PURPOSE: Orders the current wizard sidepane to go to the input step. If no wizard is active, this'll have no effect.

PARAMETERS: Number stepID — Number of the step the wizard is moving to.

RETURNS: Nothing.

pDisplayHelp(goalAreaID, taskID)

PURPOSE: Navigates to the specified sidepane's Help page. The Help page is specified by the URL element of the sidepane TaskHelp attribute in the XML content schema.

PARAMETERS: Number goalAreaID — ID of the goal area containing the target sidepane.

Number taskID — Sidepane ID as found in the TaskID field of its XML node.

RETURNS: Nothing.

pHelpLaunch(contextID)

PURPOSE: Uses the Application.HelpLaunch function to bring up Microsoft Project Help for the input contextID.

PARAMETERS: Number contextID — ID of the context whose help information should be displayed.

RETURNS: Nothing.

Tim Harahan is a developer on the Microsoft Project team. Tim helped implement the Project Guide, and is glad to finally see it in action.