Click here to Skip to main content
Click here to Skip to main content

Creating Custom Build Components for Sandcastle

, 17 May 2007 Ms-PL
Rate this:
Please Sign up or sign in to vote.
This shows how to create build components for use with Sandcastle's BuildAssembler tool to customize the help file topics that it generates.

Sample Image - Sandcastle.jpg

Table of Contents

Introduction

Sandcastle is the tool used for creating MSDN-style documentation from .NET assemblies and their associated XML comments files. The current version is the March 2007 CTP. My first article about Sandcastle describes the Help File Builder that acts as a front end to make it easier to manage and build help files using it. This article will describe how you can customize the BuildAssembler.exe tool to add new features and manipulate the actual help topics as they are being generated. This is accomplished through the use of build components. The build components presented in this article can be used with your own build scripts or manual build processes. In addition, they are integrated into the help file builder so that they can be used there as well. Information is also given on how you can integrate your own build components with the help file builder. The project is hosted at CodePlex in the Sandcastle Help File Builder project. Go there for the latest release, source code, the issue tracker, and discussion boards.

Build Component Basics

The first steps in building a help file with Sandcastle consists of running the MRefBuilder.exe tool to create the reflection information file. The XslTransform.exe tool is then used to perform a series of XSL transformations on that information to produce a manifest file and to add supporting information to the reflection information such as overload information and output filenames. The manifest file (manifest.xml) consists of a set of topic entries, one for each item to document.

<?xml version="1.0" encoding="utf-8"?>
<topics>
  <topic id="R:Project" />
  <topic id="N:TestDoc" />
  <topic id="T:TestDoc.BaseTest" />
  <topic id="M:TestDoc.BaseTest.#ctor" />
  <topic id="M:TestDoc.BaseTest.Help" />
  <topic id="T:TestDoc.ExplicitImplementation" />
    .
    .
    .
</topics>

The ID of each topic corresponds to an element in the reflection information file (reflection.xml). The data in reflection.xml represents the namespaces, types, methods, properties, etc. from the documented assemblies.

    .
    .
    .
    <api id="M:TestDoc.BaseTest.#ctor">
      <apidata name=".ctor" group="member"
        subgroup="constructor" />
      <memberdata visibility="public" static="false"
        special="true" />
      <containers>
        <library assembly="TestDoc" module="TestDoc" />
        <namespace api="N:TestDoc" />
        <type api="T:TestDoc.BaseTest" ref="true" />
      </containers>
      <file name="M_TestDoc_BaseTest__ctor" />
    </api>
    .
    .
    .

BuildAssembler is then used to convert the reflection information into a set of HTML files that represent the help topics for each item in the manifest file. It is passed the name of a configuration file using the /config command line option and the name of the manifest file.

BuildAssembler /config:sandcastle.config manifest.xml

The configuration file (sandcastle.config) defines a list of components referred to as "the component stack." BuildAssembler creates an instance of each defined component and then, for each topic in the manifest, it creates an in-memory XML document and passes it to each component in turn so that they can modify it any any way that they see fit. The basic structure of the configuration file is shown here.

<configuration>
  <dduetools>
    <builder>
      <components>

        <!--<span class="code-comment"> Components can have configuration data --></span>
        <component type="type" assembly="path">
            <!--<span class="code-comment"> Component configuration data --></span>
        </component>

        <!--<span class="code-comment"> Or they may not --></span>
        <component type="type" assembly="path" />

        <!--<span class="code-comment"> Other components --></span>
        .
        .
        .

      </components>
    </builder>
  </dduetools>
</configuration>

The type attribute is the fully qualified name of the class to create. The assembly attribute specifies the path and filename of the assembly that contains the build component class. If a component has configuration data, it is nested within the component tag. Some of the more complex components actually define nested build components that are run conditionally. Also note that a build component can appear more than once in a configuration file. Each occurrence represents a unique instance and each can be configured to do different things if necessary.

If you would like to see what the document looks like before and/or after each component has processed it, add the DisplayComponent to the component stack in the desired locations using this configuration element (lines wrapped for display purposes):

<component type="Microsoft.Ddue.Tools.DisplayComponent"
    assembly="C:\Program Files\Sandcastle\ProductionTools\
BuildComponents.dll" />

This will cause BuildAssembler to dump the current document's XML to the console. This is quite useful when you want to see the structure of the document to find out how to extract the information in which you are interested. Be warned that they can be quite large and even for an average sized project, it can result in tens of megabytes of log information for an entire project.

The Default Component Stack

The standard component stack consists of the following items in this order.

CopyFromFileComponent (Create skeleton document)
This component creates a skeleton XML document to use for the help topic. It looks like this:
<document>
    <reference />
    <syntax />
    <comments />
</document>

The reference section will contain reflection information for the topic (for example data from reflection.xml). The syntax section will contain information used to generate the language syntax information for the help topic. The comments section will contain the XML comments for the help topic. These are merged in from your assemblies' XML comments files and this element will probably be the one that you are most interested in modifying.

CopyFromFileComponent (Copy in reflection information)
This is another instance of the CopyFromFileComponent. This instance is used to copy reflection data into the topic from the reflection information files (the one created by MRefBuilder and additional files related to the .NET Framework).
CopyFromIndexComponent (Copy in container data)
This component adds data to the reference section that identifies from which assembly the reflection information came as well as the containing namespace and type information.
ForEachComponent (Copies in more data based on an expression)
This component contains a nested CopyFromIndexComponent that is run for each item found by the defined expression. It adds additional API data to the document's reference section.
IfThenComponent/SyntaxComponent (Generate syntax)
The SyntaxComponent is nested within an IfThenComponent. The IfThenComponent can be used to conditionally run one or more components based on the results of a condition defined using an XPath query. In this case, it only runs the syntax component if the topic does not start with "Overload:" or "R:". Those topics will not contain a syntax section. For topics that do contain a sytnax section, the SyntaxComponent generates a language entry using each of the nested generator elements.
CopyFromIndexComponent (Copy in XML comments)
This instance of the CopyFromIndexComponent will merge your XML comments and related .NET Framework comments into the topic for the given member that it represents.
ForEachComponent, various nested components (Copy in reflection data and member comments)
This is probably the most complex looking component definition in the standard configuration file. It uses a set of nested components to copy various supporting comment information into the reference and comments section of the document.
TransformComponent (Transform the document via XSLT)
In the standard configuration, this step converts the XML document into HTML. The configuration data for this component passes in parameters that determine whether or not index information is generated and which languages should appear in the syntax section. After this step, the elements of the skeleton document created in the prior steps are gone and in their place you will find an HTML document. Note that the HTML is still treated as XML (i.e. it is well-formed, all tags are closed, etc). This is important to remember if you modify the information after this step. It also has a few conditions to be aware of which I will describe later.
SharedContentComponent (Resolve shared content items)
This component is used to merge shared content into the document. Shared content can be found in the files shared_content.xml, reference_content.xml, and syntax_content.xml. These contain a set of messages that are common to all help files such as section headers, footers, labels, etc. This component will convert placeholders for these items into the actual text from the shared content files. By replacing these shared content files, you can change the wording or convert them to a different language without having to change the transformation files or the build procedure.
ResolveReferenceLinksComponent (Resolve links to internal and external help topics)
This component is used to resolve links to other help topics in the help file (i.e. links to help for other type members in the documented assembly) and to external content such as online help for .NET Framework class members.
SaveComponent (Save the result)
As its name implies, this final component is used to save the results of the build process to an output file. It uses an XPath expression to get the filename from the document content and merges it with an output path and extension. In the case of the default configuration, it also prevents the content from being indented and does not output the XML document header in the file.

There are many other build components supplied with Sandcastle. Unfortunately, there is not that much documentation about what they do or how to use them yet so it usually takes a bit of hunting around with Reflector to figure out what they do, if you can use them, and how to configure them.

Creating a Build Component

This section will walk you through the creation of a custom build component.

Defining the Component

The first step in creating your own build components is determining where in the component stack to place your component definition. The components are invoked in the order that they appear in the configuration file. As such, you can insert your own components anywhere in the stack based on the task that you want to perform. For the most part, you will most likely insert your components just before or just after the TransformComponent. To modify the document information or XML comments before they are converted to HTML, place your component ahead of the TransformComponent. To modify the transformed HTML, place it after the TransformComponent component.

The next step is to determine what to enter into the configuration file to define your component and its parameters. In its most basic form, a build component that takes no parameters is entered as follows (lines wrapped for display purposes):

<component type="Microsoft.Ddue.Tools.DisplayComponent"
    assembly="C:\Program Files\Sandcastle\ProductionTools\
BuildComponents.dll" />

The type attribute should specify the fully qualified component name including the namespace. The assembly attribute should specify the path and filename of the assembly that contains the component. This can be an absolute path or a relative path based on the location from which you perform the build.

For a component that has configuration data, specify the information in one or more nested elements within the component tag. In general, you will specify an element and use attributes to define one or more values to use in the component. Here is an example configuration for the CodeBlockComponent. Note that you can include comments to document the configuration for any other users of your component and to document references to any other dependent components that may be needed.

<!--<span class="code-comment"> Code block component configuration.  This must appear before
     the TransformComponent.
     See also: PostTransformComponent. --></span>
<component type="SandcastleBuilder.Components.CodeBlockComponent"
  assembly="..\bin\Debug\SandcastleBuilder.Components.dll"
  id="Code Block Component">
    <!--<span class="code-comment"> Base path for relative filenames in source
         attributes (optional) --></span>
    <basePath value="C:\DotNet20\CS\TestDoc\Doc" />

    <!--<span class="code-comment"> Code colorizer options (required).
         Attributes:
            Language syntax configuration file (required)
            XSLT style file (required)
            Default language (optional)
            Enable line numbering (optional)
            Enable outlining (optional)
            Tab size override (optional,
                0 = Use syntax file setting --></span>
    <colorizer syntaxFile="highlight.xml"
        styleFile="highlight.xsl" language="cs"
        numberLines="true" outlining="true"
        tabSize="0" />
</component>

In this example, the basePath element has a single value attribute. The colorizer element has several attributes to define its options. You may also have noticed the id attribute on the component tag. This is used by the Sandcastle Help File Builder and will be explained later.

Creating the Project

This section describes how to create and configure the build component project. I will describe the process for a C# project but the steps should be fairly similar for a VB.NET project with a few differences in the configuration option titles.

  • In Visual Studio, create a new Class Library project. Once it has been created, right click on the project and select Properties.
  • In the Application tab, set the assembly name and default namespace as you see fit.
  • On the Debug tab, set the Start Action to "Start external program" and enter the path to the BuildAssembler.exe program. If you installed Sandcastle in the default location, this will be C:\Program Files\Sandcastle\ProductionTools\BuildAssembler.exe. If not, substitute your path as appropriate.
  • In the Start Options section on the Debug tab, enter "/config:sandcastle.config manifest.xml" for the "Command line arguments" option.
  • Create a folder under your project directory such as "TestComponent" to use as the debugging location. Enter the full path to this location in the "Working directory" option. Testing and debugging will be covered later.
  • Select the Signing tab and check the "Sign the assembly" checkbox. Select "<New...>" from the "Choose a strong name key file" dropdown, enter a filename, and click OK to create the key file. You can protect the key file with a password if you like or uncheck the option to create one without a password.

The next step is to add the necessary assembly references.

  • Right click on the References folder in the project and select "Add Reference...".
  • On the .NET tab, scroll down the list and select the System.Configuration assembly. This contains the ConfigurationErrorsException class which is often thrown during construction to indicate a problem with the component configuration.
  • Open the Add Reference dialog box again. This time, select the Browse tab. Navigate to your Sandcastle installation folder (usually C:\Program Files\Sandcastle\) and then into the ProductionTools folder. Select BuildAssemblerLibrary.dll and click OK to add it to the references. This contains the base class for the build component and some supporting definitions. Once added, view its properties and set the Copy Local option to false. This file is loaded from the Sandcastle installation folder by BuildAssembler and should not be copied locally or distributed with the build components.
  • If you would like to make your component interactively configurable from the Sandcastle Help File Builder, open the Add Reference dialog and add a reference to System.Windows.Forms from the .NET tab. More information about this will be supplied later.

You can add other necessary references to the project as you develop your build component.

The Build Component Skeleton Class

You are now ready to create the build component class itself. Add a new class to the project and insert the following skeleton code into it. Here again, I am only going to show C# code. For VB.NET, the structure will be similar but you will have to convert it to the VB.NET syntax. Each part will be covered in detail below.

using System;
using System.Configuration;
using System.Windows.Forms;     // For SHFB configuration
using System.Xml;
using System.Xml.XPath;

// TODO: Add other using statements as needed

using Microsoft.Ddue.Tools;     // Build component namespace

// TODO: Alter namespace to match the project.  Also change the
//       class and constructor name accordingly.
namespace Test.Components
{
    /// <span class="code-SummaryComment"><summary></span>
    /// TODO: Give a summary of your build component
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><remarks>TODO: Add remarks or describe component</span>
    /// details.<span class="code-SummaryComment"></remarks></span>
    /// <span class="code-SummaryComment"><example></span>
    /// <span class="code-SummaryComment"><code lang="xml" title="Example configuration"></span>
    ///     TODO: Add example XML configuration section here.
    /// <span class="code-SummaryComment"></code></span>
    ///
    /// TODO: Add other example usage if needed.
    ///
    /// <span class="code-SummaryComment"></example></span>
    public class ExampleComponent : BuildComponent
    {
        #region Private data members
        //=======================================================
        // Private data members

        // TODO: Add private data members here

        #endregion

        #region Constructor
        /// <span class="code-SummaryComment"><summary></span>
        /// Constructor
        /// <span class="code-SummaryComment"></summary></span>
        /// <span class="code-SummaryComment"><param name="assembler">A reference to the</span>
        /// build assembler.<span class="code-SummaryComment"></param></span>
        /// <span class="code-SummaryComment"><param name="configuration">The configuration</span>
        /// information<span class="code-SummaryComment"></param></span>
        /// <span class="code-SummaryComment"><exception cref="ConfigurationErrorsException"></span>
        /// This is thrown if an error is detected in the
        /// configuration.<span class="code-SummaryComment"></exception></span>
        public ExampleComponent(BuildAssembler assembler,
          XPathNavigator configuration) :
            base(assembler, configuration)
        {
            // TODO: Add code to extract configuration options
        }
        #endregion

        #region Apply the component
        /// <span class="code-SummaryComment"><summary></span>
        /// This is implemented to perform the component tasks.
        /// <span class="code-SummaryComment"></summary></span>
        /// <span class="code-SummaryComment"><param name="document">The XML document with</span>
        /// which to work.<span class="code-SummaryComment"></param></span>
        /// <span class="code-SummaryComment"><param name="key">The key (member name) of the</span>
        /// item being documented.<span class="code-SummaryComment"></param></span>
        public override void Apply(XmlDocument document,
          string key)
        {
            // TODO: Add code to perform the build
            //       component task(s)
        }
        #endregion

        #region Static configuration method for use with SHFB
        /// <span class="code-SummaryComment"><summary></span>
        /// This static method is used by the Sandcastle Help
        /// File Builder to let the component perform its own
        /// configuration.
        /// <span class="code-SummaryComment"></summary></span>
        /// <span class="code-SummaryComment"><param name="currentConfig">The current</span>
        /// configuration XML fragment<span class="code-SummaryComment"></param></span>
        /// <span class="code-SummaryComment"><returns>A string containing the new</span>
        /// configuration XML fragment<span class="code-SummaryComment"></returns></span>
        public static string ConfigureComponent(
          string currentConfig)
        {
            // TODO: Add code to show configuration dialog box

            return currentConfig;
        }
        #endregion
    }
}

The component is derived from the Microsoft.Ddue.Tools.BuildComponent class and consists of a required constructor and a required Apply method. The static ConfigureComponent method is one of my own additions and is optional. The class also includes an inherited utility method (WriteMessage) described below. Review the code above for "TODO:" comments to find sections that need attention such as changing the namespace and class name etc.

The WriteMessage Method

The WriteMessage method is a utility method inherited from the base class. You can use it at any time to report information, warning, and error messages. It takes a MessageLevel enumeration value (Info, Warning, or Error), and a message string.

// Report version information at construction
Assembly asm = Assembly.GetExecutingAssembly();
FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(
    asm.Location);

base.WriteMessage(MessageLevel.Info, String.Format(
    CultureInfo.InvariantCulture,
    "\r\n[{0}, version {1}]\r\nPost-Transform Component. " +
    "Copyright (c) 2006, Eric Woodruff, All rights reserved.",
    fvi.ProductName, fvi.ProductVersion));


// Report a warning
base.WriteMessage(MessageLevel.Warn,
    "Unable to locate cache file.  The cache will be ignored.");


// Report an error
base.WriteMessage(MessageLevel.Error,
    "<head> section not found!  Could not insert links.");

It is important to note that as currently written, if you use MessageLevel.Error, the build process is terminated immediately. However, in looking at the code for the existing build components with Reflector, they are written with the expectation that the call will return and the build process will carry on after reporting an error. As such, it would be best to follow their example and assume that the build process may continue in case the current behavior changes in the future. If the condition you are reporting is fatal for all future calls as well, then it would probably be best to throw an exception to stop the build process instead.

The Constructor

The constructor is called during the startup phase by BuildAssembler. Each component is constructed in the order listed in the configuration file. The constructor is passed a reference to the build assembler and an XPathNavigator that contains the component's configuration information. This is the place to load your configuration data and perform any other required initialization steps.

// Assume the configuration is as follows:
//
// <component type="Test" assembly="Test.dll">
//     <basePath value="C:\DotNet20\CS\TestDoc\Doc" />
// </component>
//
public ExampleComponent(BuildAssembler assembler,
  XPathNavigator configuration) :
    base(assembler, configuration)
{
    XPathNavigator nav;
    string basePath = null;

    // Retrieve the "basePath" element
    nav = configuration.SelectSingleNode("basePath");

    // If found, get the content of the "value" attribute
    if(nav != null)
        basePath = nav.GetAttribute("value", String.Empty);

    // If not specified, use the current directory as
    // the default.
    if(String.IsNullOrEmpty(basePath))
        basePath = Directory.GetCurrentDirectory();
}

The assembler parameter does not have any use beyond passing it to the base class constructor. The configuration parameter is used to retrieve any settings that you may need by using XPath queries. A full discussion of XPath queries and how to use the navigator is beyond the scope of this article. The code example above is extracted from the supplied build components that have simple configurations with basic lookups to retrieve their parameters. This should suffice for most components. This article does not cover more complex configurations such as those with nested or repeated elements.

To get a configuration element, call configuration.SelectSingleNode and pass it the name of the element to retrieve. This returns an XPathNavigator object for the element if it is found or null if it is not found. To retrieve attribute values from the element, use the GetAttribute method on the returned navigator passing it the name of the attribute. The second parameter represents a namespace URI and, unless you are using XML namespaces in your configuration, the second parameter will always be String.Empty. The content of the attribute is returned as a string. If the attribute is not found, the method returns an empty string.

The example above allows the basePath element and/or its value attribute to be omitted from the configuration. In the case where a required element or attribute is not found, you should throw an exception to terminate the build. The most common exception to throw in such cases is ConfigurationErrorsException passing it a message describing the problem.

The Apply Method

This is an abstract method and must be overridden. For each topic in the manifest file, BuildAssembler calls this method in each component in the order they are listed in the configuration file so that they can modify the content of the topic.

public override void Apply(XmlDocument document, string key)
{
    XmlNodeList codeList;
    string codeBlock;
    bool nbrLines;

    // Select all code nodes
    codeList = document.SelectNodes("//code");

    foreach(XmlNode code in codeList)
    {
        nbrLines = false;

        // If there's a source attribute, load the code
        // from the file.
        if(code.Attributes["source"] != null)
            codeBlock = this.LoadCodeBlock(code);
        else
            codeBlock = code.InnerXml;

        // Check for option overrides
        if(code.Attributes["numberLines"] != null)
            nbrLines = Convert.ToBoolean(
                code.Attributes["numberLines"].Value,
                CultureInfo.InvariantCulture);

        .
        .
        .
        .

        // Save the changes
        code.InnerXml = codeBlock.Substring(start,
            end - start).Replace(" ", "&#x20;");

    }
}

The document parameter represents an XML document containing the information that can be modified. The key parameter contains the ID of the topic. The ID will match one of the values found in the manifest file. Again, XPath queries are used to select information from the document and modify it. The above code selects all code elements from the document and looks for attributes on them to use as values that modify how the code colorizer works. The XmlDocument differs from the XPathNavigator with regard to accessing attributes. You retrieve them via the Attributes property. If not found, null is returned rather than an empty string. The value of the attribute is retrieved as a string via its Value property.

Changes to the document can be stored back to the element's InnerXml or InnerText property. More advanced components can rewrite the XML using an XPathNavigator. Again, that is beyond the scope of this article and is left as an exercise for the reader.

Some HTML Issues

As noted earlier, the document is manipulated as XML. As such, this does affect how the document is written once it has been transformed into HTML or if you insert HTML tags into the document prior to transformation. The following are some issues that I ran into while developing the build components supplied with this article:

  • Non-breaking spaces ( ) are stripped from the document. If you need to insert spaces into the document, you will need to convert them into space entities instead (&#x20;). The side-effect is that when written to the saved document, they are written as literal spaces. This may affect the layout of the text. In the case of the code colorizer, the spaces I had to convert where in a pre tag and used a fixed font so it was not an issue. It appears to be an XML or an XSLT problem and I could not find a workaround other than converting the spaces as shown in this example.
    // Non-breaking spaces are replaced with a space entity.  If
    // not, they disappear in the rendered HTML.  Seems to be an
    // XML or XSLT thing.
    code.InnerXml = codeBlock.Substring(start,
        end - start).Replace(" ", "&#x20;");
    
  • If you insert tags into the document, they must be well-formed (i.e. they must have a closing tag or be self-closing).
  • Certain HTML tags cannot be self-closing. The span, div, and script tags are three common ones. If you add these tags to a document but do not manually set their inner text to an empty string, they are automatically written out as self-closing tags which will not render correctly in Internet Explorer and perhaps other browsers. Below are two examples of fixing up the tags:
    // <span> tags cannot be self-closing if empty.  The
    // colorizer renders them correctly but when written out as
    // XML, they get converted to self-closing tags which breaks
    // them.  To fix them, store an empty string in each empty
    // span so that it renders as an opening and closing tag.
    XmlNodeList spans = code.SelectNodes("//span");
    
    // Note that if null, InnerText returns an empty string
    // by default.  As such, this looks redundant but it
    // really isn't (see note above).
    foreach(XmlNode span in spans)
        if(span.InnerText.Length == 0)
            span.InnerText = String.Empty;
    
    
    
    // Add the link to the script
    node = document.CreateNode(XmlNodeType.Element, "script", null);
    
    attr = document.CreateAttribute("type");
    attr.Value = "text/javascript";
    node.Attributes.Append(attr);
    
    attr = document.CreateAttribute("src");
    attr.Value = Path.GetFileName(scriptFile);
    node.Attributes.Append(attr);
    
    // Script tags cannot be self-closing so set their inner text
    // to an empty string so that they render as an opening and a
    // closing tag.  Note that if null, InnerText returns an empty
    // string.  This looks redundant but it isn't.
    node.InnerText = String.Empty;
    
    head.AppendChild(node);
    

    As noted in the comments, the InnerText property of an XmlNode will return an empty string if it contains a null. However, it will still render as a self-closing tag. By manually storing an empty string to the property, we can make it write out both an opening and a closing tag. It looks redundant, but it really is needed.

The ConfigureComponent Method

The ConfigureComponent method is not part of the standard build component. This is a method that I added in order to support interactive configuration of custom build components from within the Sandcastle Help File Builder. By letting the components edit their own configuration data, I do not have to add their properties to the project file directly and I can support build components written by other people. The method is declared public and static (shared) and is passed a string containing the current component definition as it appears in the configuration file. It should return the configuration to use for the project as a string containing the modified XML. The XML configuration string is stored in a collection in the project and will replace the default configuration at build time. The following example method simply invokes a modal dialog box to edit the configuration. The dialog box is passed the string in its constructor and parses the XML to populate the controls that it contains. If OK is clicked, the dialog's Configuration property is used to return an XML string containing the modified information.

public static string ConfigureComponent(string currentConfig)
{
    // Open the dialog to edit the configuration
    using(CodeBlockConfigDlg dlg =
      new CodeBlockConfigDlg(currentConfig))
    {
        // Get the modified configuration if OK was clicked
        if(dlg.ShowDialog() == DialogResult.OK)
            currentConfig = dlg.Configuration;
    }

    // Return the configuration data
    return currentConfig;
}

To get the Sandcastle Help File Builder to recognize the component as configurable, just add a unique id attribute to the component tag for the component. The default component configuration should be added to the sandcastle.config file found in the Templates\ folder under the help file builder's installation folder. The ID value is used as the title in the dialog box used to select the component to configure.

Testing and Debugging

In order to test and debug your project be sure that you have followed the instructions in the Creating the Project section to configure the debug properties. Next, you will need to run at least one help file build to get some test files to put in the working folder specified in the project debug options. If you are using the Sandcastle Help File Builder, set the CleanIntermediates property to false so that you can copy the files from the .\Working folder after the build. At the minimum, you will need:

  • sandcastle.config - The Sandcastle configuration file
  • manifest.xml - The manifest file.
  • reflection.xml - The reflection information file
  • The XML comments files for the build
  • The shared content files (shared_content.xml, reference_content.xml, and syntax_content.xml).
  • The contents of the Output\ folder (icons, scripts, styles, etc).

Once you have the necessary files in place, edit the sandcastle.config file to add the component entry for your custom build component. As mentioned earlier, you can also add instances of the DisplayComponent in order to view the document before and after your component runs.

Debugging the component code is similar to any other project. You can set breakpoints anywhere in the code, step through it, examine the variables, etc. In the case of the XML document, the Visual Studio 2005 debugger visualizers are very helpful in letting you see the document structure at runtime.

The Supplied Components

The next few sections will describe the build components supplied with this article. Information is supplied on how to configure them and use them in builds other than those done with the help file builder. I will not cover the code for each component in detail. They are pretty straightforward and the sections above describe the basic operations that you will see within them.

The Show Missing Documentation Component

The "show missing documentation" build component is used to add "missing" notes for missing summary, parameter, returns, value, and remarks tags. It can also add default summary documentation for constructors. The functionality in this component used to be integrated with the help file builder itself. However, the XPath queries used to look up the information in the comments files and reflection information file proved to be too slow when used on extremely large builds with tens of thousands of topics. By moving the processing to a build component, the amount of information to search is greatly reduced and the time to add missing item comments is now negligible.

The Show Missing Documentation Component Configuration

The following is the default configuration for the "show missing documentation" component. It should be inserted into the configuration file before the TransformComponent. All attribute names and values are case-sensitive.

<!--<span class="code-comment"> Show missing documentation component configuration.  This must
     appear before the TransformComponent. --></span>
<component type="SandcastleBuilder.Components.ShowMissingComponent"
  assembly="{@SHFBFolder}SandcastleBuilder.Components.dll">
    <!--<span class="code-comment"> All elements are optional. --></span>
    <!--<span class="code-comment"> Auto-document constructors (true by default) --></span>
    <AutoDocumentConstructors value="{@AutoDocumentConstructors}" />

    <!--<span class="code-comment"> Show missing param tags (true by default) --></span>
    <ShowMissingParams value="{@ShowMissingParams}" />

    <!--<span class="code-comment"> Show missing remarks tags (false by default) --></span>
    <ShowMissingRemarks value="{@ShowMissingRemarks}" />

    <!--<span class="code-comment"> Show missing returns tags (true by default) --></span>
    <ShowMissingReturns value="{@ShowMissingReturns}" />

    <!--<span class="code-comment"> Show missing summary tags (true by default) --></span>
    <ShowMissingSummaries value="{@ShowMissingSummaries}" />

    <!--<span class="code-comment"> Show missing value tags (false by default) --></span>
    <ShowMissingValues value="{@ShowMissingValues}" />
</component>

The example given above is taken from the Sandcastle Help File Builder's configuration file. When used with it, the replacement tag {@SHFBFolder} is used to insert the help file builder's folder in the file path. This is replaced at build time with the appropriate value. The other elements also use replacement tags that map to the help file builder project options of the same name. If using the component in your own build scripts, replace the tags with actual values. All elements are optional. If not specified, the default value noted in the comment before the element is used.

AutoDocumentConstructors

This is used to indicate whether or not constructors are automatically documented if they are missing the <summary> tag and for classes with compiler-generated constructors. Set this to true (the default) to automatically add default text for the <summary> tag on constructors that are missing it and for classes with a compiler-generated constructor. If set to false and ShowMissingSummaries is true, a "missing summary" warning will appear instead. A message is also written to the log file when a constructor is auto-documented.

ShowMissingParams

This property lets you indicate whether or not a red "missing" message is inserted into the help topic for each method parameter that is missing its related <param> tag documentation. The default is true to add the message.

ShowMissingRemarks

This property lets you indicate whether or not a red "missing" message is inserted into the help topic if the <remarks> tag has not been specified for the item. The default is false to ignore items with missing or empty <remarks> tags.

ShowMissingReturns

This property lets you indicate whether or not a red "missing" message is inserted into the help topic if the method returns a value and the <returns> tag has not been specified for the item. The default is True to add the message.

ShowMissingSummaries

This property lets you indicate whether or not a red "missing" message is inserted into the help topic if the <summary> tag has not been specified for the item. The default is True to add the message.

ShowMissingValues

This property lets you indicate whether or not a red "missing" message is inserted into the help topic for property items that are missing the <value> tag documentation. The default is false to ignore properties with missing or empty <value> tags.

The Version Information Component

The version information component is used to extract version information from the reflection information file and pass it on to the PostTransformComponent so that it can insert it into the help topic. The functionality in this component used to be integrated with the PostTranformComponent itself. However, looking up the version information after transformation of the topic proved to be too slow. Moving the lookup so that it occurs before transformation allows for an optimized lookup of the version information and the whole reflection file does not need to be loaded thus reducing memory usage for large builds and speeding up the searches for version information.

The Version Information Component Configuration

The following is the default configuration for the version information component. It should be inserted into the configuration file before the TransformComponent. As noted earlier, this component must be paired with the PostTransformComponent as it is used to insert the version information into the topic after the transformation to HTML.

<!--<span class="code-comment"> Version information component configuration.  This must appear
     before the TransformComponent.  See also: PostTransformComponent --></span>
<component type="SandcastleBuilder.Components.VersionInfoComponent"
  assembly="{@SHFBFolder}SandcastleBuilder.Components.dll">
    <!--<span class="code-comment"> Reflection information file for version info (required) --></span>
    <reflectionFile filename="reflection.xml" />
</component>

The example given above is taken from the Sandcastle Help File Builder's configuration file. When used with it, the replacement tag {@SHFBFolder} is used to insert the help file builder's folder in the file path. This is replaced at build time with the appropriate value. If using the component in your own build scripts, replace the tag with a relative or absolute path to the component assembly. The reflectionFile element is required and should contain the path to the reflection information file.

The help file builder contains the necessary modifications to its reference content files to add version information. If you use this component in your own scripts or build tools, you will need to modify the Sandcastle reference content files in order to add the version parameter. To do so, add the text "Version: {2}" to the locationInformation entry in the reference_content.xml file for the Prototype and VS2005 styles and the requirementsAssemblyLayout entry in the reference_content.xml file for the VS2005 style.

The Code Block Component

The Code Block Component is used to extend the functionality of the XML comments <code> tag. It provides the following features:

  • Excess leading whitespace is stripped from the <code> blocks to left-align them correctly.
  • It adds support for optional line numbering.
  • It adds support for optional collapsible regions for code in #region and #if/#else/#endif blocks with support for nested collapsible regions. The VB.NET equivalents are supported too.
  • A default title can be added based on the language that the code represents.
  • A "Copy" link appears to the right on the title line that allows you to copy the code sample to the clipboard (Internet Explorer only). The code is copied as plain text without the highlighting and line numbers if used.
  • It adds support for reading in an external source file or a defined region of an external source file so that you can keep code samples in a buildable project to test them for correctness and to do away with managing the code samples in the XML comments or include files.
  • C#, VB.NET, C++, and J# code blocks can be associated with the language filter to show or hide them based on the language filter setting.
  • Syntax highlighting of code blocks in <code> tags. Languages supported include C#, VB.NET, C++, J#, C, JavaScript, VBScript, and XML. An external configuration file is used so that it is possible to extend the colorizer to support other languages. The stylesheet is also replaceable. All of the features noted above are available whether or not you enable the colorizing feature.

The Code Block Attributes

The code colorizer is a modified and extended version of the one written by Jonathan de Halleux in the Code Project article Multiple Language Syntax Highlighting, Part 2: C# Control [^]. The modified code is available with the source download for the help file builder and the build components. The following attributes can be added to the <code> tag. They can be used in any combination with each other with the exception of region which, if used, must be paired with a source attribute. All attribute names and values are case-sensitive.

The source and region Attributes

This feature is based on an idea suggested by Morten Nielsen in the MSDN Documentation Forum. The source attribute is used to specify that the code block's content should be read from an external source code file. If used alone, the entire file is used. The optional region attribute can be used to limit the code to a specific section of the file delimited with the named #region. The #region and #endregion tags are excluded from the extracted section of code.

This is not to be confused with the XML comments <include> tag. This extension is intended to extract code from actual source files. This allows you to manage your code examples in buildable projects to test them for correctness as a project is developed and altered in the future. It also saves you from managing the code in the XML comments and does not require that the code be HTML encoded as it is when written in the comments. The code will be HTML encoded when it is read in for processing. When used, it is assumed that there is no code within the comment tag itself, it will always be self-closing. Here are some examples:

Retrieve all code from an external file and use the VB.NET syntax to color it. The path is relative to the basePath configuration element.

<code source="..\Examples\WholeDemo.vb" lang="vbnet" />

Retrieve a specific #region from an external file.

<code source="..\Examples\SeveralExamples.vb"
    region="Example 1" lang="vbnet" title="Example #1" />

Note that VB.NET does not allow #Region and #End Region within a method body. However, if you want to extract a region from a VB.NET method body, you can add the #Region and #End Region statements in comments to workaround the limitation. The component will still find it and extract the region.

The lang Attribute

This attribute allows you to override the default language specified in the component's configuration. Any <code> tag without a lang attribute will use the value specified in the component's configuration. For example, if most or all of your code examples are in C#, you can set the default language in the configuration to "cs". If you have an example in VB.NET, you can add lang="vbnet" to colorize that example as VB.NET code. The possible language values in the supplied configuration file are as follows:
  • cs = C#
  • cpp = C++
  • c = C
  • jscript = JavaScript
  • vbnet = VB.NET
  • vbscript = VBScript
  • jsharp = J#
  • xml = XML
  • Anything else (i.e. "none") = No highlighting

The numberLines Attribute

This attribute allows you to override the default setting in the component's configuration. For example, if the default setting is false to turn off line numbering, you can add numberLines="true" to enable numbering on a specific code example.

The outlining Attribute

This attribute allows you to override the default setting in the component's configuration. For example, if the default setting is false to not add collapsible regions, you can add outlining="true" to enable collapsible regions on a specific code example. Note that if a code block contains no #region or #if blocks, outlining is automatically disabled and it will not reserve space in the margin for the markers.

The tabSize Attribute

When the code blocks are formatted, tab characters are replaced with a set number of spaces to preserve formatting. This attribute can be used to override the default setting for a language which is specified in the syntax file. For example, if the default tab size for a language is four, adding tabSize="8" will force it to use eight spaces instead. If set to zero, the syntax file setting is used. This attribute has the same behavior when used in the component's configuration.

The title Attribute

This attribute allows you to add a title that appears before the code block. An example of its use would be to label the example with a description. If omitted and the defaultTitle attribute on the code block component's colorizer element is true, the language name will appear for the title. If it is set to false, no title will appear. If using default titles and you do not want a title on a particular block, set the title attribute to a single space (" ").

NDoc had an additional escaped attribute that let you enter literal XML in the comments block without having to HTML encode things like the angle brackets. An attempt was made to support that attribute in the code block component. However, the XML document as created by Sandcastle and passed to the component has all inter-element whitespace and line feeds stripped from it. As such, it loses all formatting in the escaped XML and renders it on a single, unbroken line. This made the escaped attribute useless and it was removed. If you want to include XML or other unencoded information, utilize the source attribute instead to read it in from an external file. Doing it this way preserves the formatting and has the same effect as the escaped attribute in NDoc.

The Code Block Component Configuration

The following is the default configuration for the code block component. It should be inserted into the configuration file just ahead of the TransformComponent. Note that the CodeBlockComponent must be paired with the PostTransformComponent as it is used to insert the script and stylesheet references in pages that have colorized code. See below for details. All attribute names and values are case-sensitive.

<!--<span class="code-comment"> Code block component configuration.  This must appear before the
     TransformComponent.  See also: PostTransformComponent. --></span>
<component type="SandcastleBuilder.Components.CodeBlockComponent"
  assembly="{@SHFBFolder}SandcastleBuilder.Components.dll"
  id="Code Block Component">
    <!--<span class="code-comment"> Base path for relative filenames in source attributes
         (optional) --></span>
    <basePath value="{@ProjectFolder}" />

    <!--<span class="code-comment"> Connect to language filter (optional).  If omitted,
         language filtering is enabled by default. --></span>
    <languageFilter value="true" />

    <!--<span class="code-comment"> Code colorizer options (required).
         Attributes:
            Language syntax configuration file (required)
            XSLT style file (required)
            "Copy" image file URL (required)
            Default language (optional)
            Enable line numbering (optional)
            Enable outlining (optional)
            Tab size override (optional, 0 = Use syntax file setting)
            Use language name as default title (optional) --></span>
    <colorizer syntaxFile="{@SHFBFolder}Colorizer\highlight.xml"
        styleFile="{@SHFBFolder}Colorizer\highlight.xsl"
        copyImageUrl="../icons/CopyCode.gif"
        language="cs" numberLines="false" outlining="false"
        tabSize="0" defaultTitle="true" />
</component>

The example given above is taken from the Sandcastle Help File Builder's configuration file. When used with it, you may specify the replacement tag {@SHFBFolder} to insert the help file builder's folder in the file path and the {@ProjectFolder} replacement tag to specify the current project's folder. These are replaced at build time with the appropriate values. These replacement tags can also be specified in the configuration dialog when configuring project-specific configurations for the component from within the help file builder. If using the component in your own build scripts, replace the tags with an actual absolute or relative path.

The basePath element is optional. If specified, it defines the base path to use for relative paths in the source attributes of code blocks. If omitted or left blank, the current folder at build time is assumed to be the base path.

The languageFilter element is optional. If omitted, or if specified and set to true, and the code block's language is C#, VB.NET, C++, or J# it will be connected to the page's language filter so that it is shown or hidden based on the language filter's setting. If the languageFilter's value is set to false, no language filtering will occur and all code blocks will be visible at the same time.

The colorizer element is required along with the syntaxFile, styleFile, and copyImageUrl attributes which define the default syntax definition file, the transformation file used by the colorizer, and the URL to use for the "Copy" image that appears to the right on the title line. The supplied highlight.xml file contains the syntax definitions for the languages supported by the colorizer. The supplied highlight.xsl file defines the XSLT transformation used to convert the parsed code block into the colorized HTML. The copyImageUrl value is the location of the image file to use for the "Copy" link. It should be specified as a path relative to the HTML output folder (normally html\. It does not have to physically exist at the time the component is used. The PostTransformComponent copies an image to the appropriate location. All other attributes are optional. If specified, they define the defaults for code blocks that do not specify the matching attribute. The optional defaultTitle attribute specifies whether or not to use the language name as the title if a specific title is not specified.

The Post-Transform Component

The Post-Transform Component is used to provide the following additional features to modify the transformed HTML for a help topic.

  • This acts as a companion component to the Code Block Component to add the supporing links for the colorizer script and stylesheet only when required.
  • It can insert a logo image at the top of each help topic. Both the Prototype and VS2005 styles are supported.
  • For the Prototype style, it hides the language combo box if only one language appears in the Syntax section and it also hooks up the code blocks to the language filter.
  • This acts as a companion component to the Version Information Component to add version information to the help topics if used with the help file builder. If used outside the help file builder, a modification is required to the Sandcastle reference content files.

The Post-Transform Component Configuration

The following is the default configuration for the post-transform component. All attribute names and values are case-sensitive. It should be inserted into the configuration file right after the TransformComponent. This component can be used by itself if you do not want to use the code block and/or version information components. However, this component is required and must be used if you do use the code block and/or version information components.

If the version information component is used, this component will insert the version information found into the help topic. For the code block component, it adds the script and stylesheet links to the document's <head> section. While they could both be inserted into the first code element by the code block component and would work, the link for the stylesheet is only considered valid HTML if it appears in the <head> section. By using this component to insert it, it keeps the HTML well formed and valid. Since it is inserting the stylesheet link, I placed the code to insert the script element in here as well. In addition, for the Prototype style it adds JavaScript to connect the code blocks up to the language filter provided that the CodeBlockComponent has been used as well. The Prototype style uses a different method for its language filter which requires some additional steps to register the code blocks so that they are shown and hidden correctly.

<!--<span class="code-comment"> Post-transform component configuration.  This must appear after the
     TransformComponent.  See also: CodeBlockComponent. --></span>
<component type="SandcastleBuilder.Components.PostTransformComponent"
  assembly="{@SHFBFolder}SandcastleBuilder.Components.dll"
  id="Post-transform Component">
    <!--<span class="code-comment"> Code colorizer files (required).
         Attributes:
            Stylesheet file (required)
            Script file (required)
            "Copy" image file (required) --></span>
    <colorizer stylesheet="{@SHFBFolder}Colorizer\highlight.css"
        scriptFile="{@SHFBFolder}Colorizer\highlight.js"
        copyImage="{@SHFBFolder}Colorizer\CopyCode.gif" />

    <!--<span class="code-comment"> Output path for the files (required).  This should match the
         output path of the HTML files (see SaveComponent below). --></span>
    <outputPath value="Output\html" />

    <!--<span class="code-comment"> Logo image file (optional).  Filename is required. The height,
         width, altText, placement, and alignment attributes are
         optional. --></span>
    <logoFile filename="" height="0" width="0" altText=""
        placement="left" alignment="left" />
</component>

The example given above is taken from the Sandcastle Help File Builder's configuration file. When used with it, you may specify the replacement tag {@SHFBFolder} to insert the help file builder's folder in the file path. This is replaced at build time with the appropriate value. This replacement tag can also be specified in the configuration dialog when configuring project-specific configurations for the component from within the help file builder. The same applies to the {@ProjectFolder} replacement tag which can be used in the logo image's filename attribute to insert a reference to the project folder. If using the component in your own build scripts, replace the tags with an actual absolute or relative path.

The colorizer element defines the files used by the code colorizer script. The stylesheet attribute defines the stylesheet to use for the colorized code. The scriptFile attribute defines the script file containing the JavaScript code used to show and hide the collapsible sections and copy the code to the clipboard (Internet Explorer only). The copyImage attribute defines the image file to use for the "Copy Code" link. The supplied highlight.css, highlight.js, and CopyCode.gif files are used by default. You can edit the stylesheet to use different colors for the highlighted code elements. If using the help file builder, you can also add a new stylesheet using the same name to the AdditionalContent property and set its destination folder to html\ to overwrite the default stylesheet. The same applies to the image file. However, its location is determined by the copyImageUrl attribute in the code block component's configuration.

The outputPath element is used to specify where the stylesheet and script files are to be copied. This should match the same output path that is used in the SaveComponent that saves the help topic files. By default, it is Output\html. The image file is copied to the location specified in the CodeBlockComponent configuration which is relative to this path. See its description for details.

The logoFile element is optional. Using it, you can add a logo image to the header of each help topic page that will appear to the left, right, or above the topic title. If no image file is specified or the element is omitted, no image will appear in the header. The height and width attributes can be used to specify the height and width of the image. If omitted or set to zero, the image will be displayed using its actual size. By using these attributes, you can scale a larger image down or scale a smaller image up to get a better fit. The configuration dialog for the component allows you to select an image and adjust its size with a preview area. The altText attribute lets you specify some alternate text that can appear for the image. The placement attribute allows you to specify where the image is placed (to the left, to the right, or above the topic title). When set to above, the alignment attribute specifies how the image is aligned (left, right, or centered).

As noted, the component can insert version information into the topic when used with the VersionInformationComponent. If not used, no version information will be listed. The help file builder contains the necessary modifications to its reference content files to add version information. If you use this component in your own scripts or build tools, you will need to modify the Sandcastle reference content files in order to add the version parameter. To do so, add the text "Version: {2}" to the locationInformation entry in the reference_content.xml file for the Prototype and VS2005 styles and the requirementsAssemblyLayout entry in the reference_content.xml file for the VS2005 style.

The Source Code

The source code is available for download from CodePlex using the link at the top of the article. Due to differences in installations of Sandcastle and Visual Studio, please be sure to read the Creating the Project and the Testing and Debugging section to fix up the assembly references and debug settings.

Conclusion

This article presents information on how to create custom build components and just a couple of ideas on how to extend Sandcastle to provide new features in the help topics. With this information, you should be able to start creating other new and interesting build components for Sandcastle as well. For a list of planned future enhancements and to make suggestions of your own, see the Issue Tracker at the Sandcastle Help File Builder's project website.

Revision History

03/11/2007 v1.4.0.0 - Changes made in this release:
  • Added support for the March 2007 CTP of Sandcastle.
  • NOTE: The default configurations for the CodeBlockComponent and PostTransformComponent have changed and must be reset and reconfigured if you customized them via the ComponentConfigurations project option. Line numbering and outlining are now off by default. A new option "Connect code blocks to language filter" has been added to allow connecting the code blocks to the language filter. If enabled (the default), code blocks are shown or hidden based on the language filter setting. If disabled, code blocks are always shown regardless of the language filter setting. Options are also present to set the location of the "Copy" image and the file path for it.
  • Added the ShowMissingComponent and VersionInfoComponent to improve overall build performance.
  • Fixed a bug in the code colorizer caused by the description for the collapsed region not being HTML encoded. Also fixed up the definitions so that it doesn't colorize preprocessor text within quotes and comments and handles XML attribute names containing colons.
  • Fixed the CodeBlockComponent and the code colorizer so that they recognize VB.NET style #Else If, #End If, and #End Region statements.
  • The code colorizer now renders a "Copy" icon and text to the right of each title line on the code blocks and allows you to copy the code to the clipboard. The code is copied as plain text without the highlighting and line numbers if used. Default titles are also used on code block that do not have a title. The default title is based on the language specified.
  • The post-transform component now turns off all file attributes when copying the highlighter and logo files so that they can be deleted at the end of the build when CleanIntermediates is set to true.
  • Added placement and alignment attributes to the logoFile element of the post-transform component. This allows the logo to be placed to the left, right, or above the topic title. When placed above the topic title, it can be aligned to the left or right or centered.
12/02/2006 v1.3.3.0 - Initial release of the build components.

For a full list of all changes in prior versions of the help file builder, see the help file supplied with the help file builder.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Eric Woodruff
Software Developer (Senior)
United States United States
Eric Woodruff is an Analyst/Programmer for Spokane County, Washington where he helps develop and support various applications, mainly criminal justice systems, using Windows Forms (C#) and SQL Server as well as some ASP.NET applications.
 
He is also the author of some open source projects and shareware components for .NET including:
 
The Sandcastle Help File Builder - A front end and project management system that lets you build help file projects using Microsoft's Sandcastle documentation tools. It includes a standalone GUI and a package for Visual Studio integration.
 
Visual Studio Spell Checker - A Visual Studio editor extension that checks the spelling of comments, strings, and plain text as you type or interactively with a tool window. This can be installed via the Visual Studio Gallery.

Image Map Controls - Windows Forms and web server controls that implement image maps.
 
PDI Library - A complete set of classes that let you have access to all objects, properties, parameter types, and data types as defined by the vCard (RFC 2426), vCalendar, and iCalendar (RFC 2445) specifications. A recurrence engine is also provided that allows you to easily and reliably calculate occurrence dates and times for even the most complex recurrence patterns.
 
Windows Forms List Controls - A set of extended .NET Windows Forms list controls. The controls include an auto-complete combo box, a multi-column combo box, a user control dropdown combo box, a radio button list, a check box list, a data navigator control, and a data list control (similar in nature to a continuous details section in Microsoft Access or the DataRepeater from VB6).
 
For more information see http://www.EWoodruff.us

Comments and Discussions

 
GeneralMissing summary in documentation - vb.net PinmemberMrWeiland17-Apr-08 1:57 
Questionerror showmissingcomponent Pinmembervoten27-Oct-07 14:57 
AnswerRe: error showmissingcomponent PinmemberEric Woodruff27-Oct-07 15:02 
AnswerRe: error showmissingcomponent Pinmembervoten28-Oct-07 10:35 
GeneralBrilliant PinmemberJoshCrosby5-Apr-07 12:49 
QuestionExample code in extra content? PinmemberNickB10-Jan-07 3:20 
AnswerRe: Example code in extra content? PinmemberEric Woodruff10-Jan-07 17:09 
GeneralRe: Example code in extra content? PinmemberNickB2-Mar-07 8:03 
GeneralRe: Example code in extra content? PinmemberEric Woodruff2-Mar-07 16:14 
GeneralRe: Example code in extra content? PinmemberEric Woodruff11-Mar-07 19:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 17 May 2007
Article Copyright 2006 by Eric Woodruff
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid