Creating a Build Engine Provider

Download the finished source code for this tutorial here: DocProject Extensibility Samples 1.0

Introduction

DocProject has powerful extensibility features that allow you to extend and override the behavior of DocProject and DocSite templates, including how they are built, what they produce and what options can be configured by users. DocProject is extensible system-wide using custom build engine providers and on a per-project basis using a Build Process Component.

Build engine providers are DocProject's most powerful point of extensibility. By implementing only a few interfaces and registering your build engine provider with DocProject, you have full control over the DocProject templates.

The build engine that you will be creating in this tutorial extends the Sandcastle build engine with new, configurable options and a new build step. It's also possible to override the Sandcastle/Deployment engine instead but for the sake of simplicity the standard Sandcastle build engine is used in this tutorial.

Sandcastle Extensibility
Sandcastle itself is also highly extensible. By using the Sandcastle build engine that is provided by DocProject, users can take advantage of Sandcastle's extensibility themselves in new DocProjects and DocSites by creating or modifying cascading style sheets and the built-in XML transformation files (XSLT).

You can even modify the build component stacks using built-in and custom Sandcastle build components. For more information, see How To Use Third-Party Build Components in DocProject.

A custom build engine provider created on top of Sandcastle:
  • can be used throughout your organization for multiple projects.
  • helps to enforce enterprise-level standards for building documentation.
  • allows for custom control over implementation for team scenarios and special requirements.
  • allows you to add or improve on existing functionality in DocProject and Sandcastle.
Building a custom build engine provider from scratch allows you to create custom build scenarios that produce output such as an internal documentation format, even without using Sandcastle, although creating a custom build engine without Sandcastle is beyond the scope of this tutorial.

Create a New Project

To begin creating a new build engine provider you should start by creating a new Class Library project in Visual Studio:
  1. Create a new Visual C# Class Library project (any language will work but we'll use C# in this tutorial):
    1. Open Visual Studio.
    2. Go to File > New > Project....
    3. Select Visual C# and Class Library.
    4. Name the project whatever you'd like and click OK. The project is created.
  2. Add a few required assembly references:
    1. Go to Project > Add Reference....
    2. Select the .NET tab.
    3. Add a reference to System.Configuration.
    4. Open the Add References dialog again.
    5. Select the Browse tab.
    6. Browse to DocProjct's bin directory, commonly found at C:\Program Files\Dave Sexton\DocProject\bin.
    7. Add references to DaveSexton.DocProject.dll and DaveSexton.DocProject.Sandcastle.dll.

Create a New Build Engine Provider

For this tutorial I'll qualify new class names with "Team", although using a more descriptive name such as the name of your company or a particular project that will be using the provider is recommended.

To create a custom build engine provider:
  1. Add a new class to the project and name it, TeamBuildEngineProvider.
  2. Derive the class from DaveSexton.DocProject.Sandcastle.SandcastleBuildEngineProvider.
  3. Override the Initialize method if you want to read properties from your provider's <add> element in DocProject's configuration file.
  4. Override the Load method if you want code to execute each time the provider is instantiated by a host such as Visual Studio or the DocProject External UI.
  5. Override the Unload method if you want code to execute each time the application that is hosting the provider closes.
  6. Override the Verify method if you want to check whether the provider should be added to the New Project Wizard's list of providers. Throw an exception if the provider cannot be used and specify a descriptive error message for end-users.
  7. Add a property to the class for each option that you want to appear on the Engines tools options page. Values assigned to these properties are shared among all DocProjects and DocSites that use your engine.
In this tutorial we'll just stick with the basics and add a single configurable property. For example, you can add a property that allows team members to configure a UNC directory where the team places external code snippets that your provider will import into new DocProjects:

Example 1
public class TeamBuildEngineProvider : SandcastleBuildEngineProvider
{
  /// <summary>Gets or sets the full path to the team share, which contains 
  /// external content that can be imported into DocProjects.</summary>
  [Category("Team Options"), DisplayName("Shared Directory"), 
  Description("Full path to the team share.")]
  public string TeamSharedDirectory
  {
    get
    {
      return Settings.Read("TeamBuildEngineProvider_TeamSharedDirectory");
    }
    set
    {
      Settings["TeamBuildEngineProvider_TeamSharedDirectory"] = value;
    }
  }
}

Notice how the provider simply reads from and writes to the Settings property. This will ensure that provider settings are persisted in the user's isolated storage area automatically.

Note: When reading and writing the value, qualifying the setting's name with the name of the build engine provider ensures that it will not conflict with property names of any base providers, although doing so is not required.

At this point you've created your first build engine provider with a property that will appear in DocProject's Engines tools options page, once the provider is registered, although it doesn't do anything useful yet.

Create a New Build Engine

In order for your provider to do anything useful you must create a custom build engine. Build engines are responsible for creating a collection of executable steps that the underlying engine will run. For more information, see Build Process.

To create a custom build engine:
  1. Add a new class and name it, TeamBuildEngine.
  2. Derive the class from DaveSexton.DocProject.Sandcastle.SandcastleBuildEngine.
  3. Add a constructor that accepts a single DaveSexton.DocProject.IDocProject argument.
Example 2
public class TeamBuildEngine : SandcastleBuildEngine
{
  public TeamBuildEngine(IDocProject project)
    : base(project)
  {
  }
}

To use the engine you must override a couple of methods in the TeamBuildEngineProvider class:
  1. Override CreatesBuildEngineOfType and return true if the specified type is the type of your build engine.
  2. Override CreateBuildEngine and return a new instance of your build engine, passing the project argument to your build engine's constructor.
Example 3
public override bool CreatesBuildEngineOfType(Type type)
{
  return type == typeof(TeamBuildEngine);
}

public override IBuildEngine CreateBuildEngine(IDocProject project)
{
  return new TeamBuildEngine(project);
}

Since the build engine created in this tutorial derives from Sandcastle's build engine, nothing more is actually required for the engine to function properly. But for the sake of example, let's make the purpose of TeamBuildEngine to copy code snippets into the project from the team's share.

Unfortunately, Sandcastle does not currently provide any transformations or a configuration file for auto-generated reference documentation that handles external code snippets out-of-the-box, so let's just assume that they are supported and that all we need to do is move the snippets into a local project folder named, Snippets so that they'll be included into XML documentation that references them.

(Note: The conceptual configuration files actually do support snippets and DocProject 1.11.0 RC provides a default snippet file that works out-of-the-box: Help\Settings\conceptual_snippets.xml. It would certainly be possible to add code here so that snippet files are automatically registered with the conceptual build component stacks after being imported, but that functionality is beyond the scope of this tutorial.)

Add code that imports shared snippet files during help builds
In this tutorial we simply want to add code that will import the snippets from the team's share into the current project. There are a few entry points into the build process but creating a custom build step will usually be the best choice:
  1. Override the CreateSteps method.
  2. Call base.CreateSteps() to obtain Sandcastle's build steps.
  3. Modify the steps collection as necessary and then return it to the caller.
Add the following code to the TeamBuildEngine class:

Example 4
protected override BuildStepCollection CreateSteps()
{
  // Get the provider's instance through the Project property
  TeamBuildEngineProvider provider = (TeamBuildEngineProvider) Project.Provider;

  string source = Path.Combine(provider.TeamSharedDirectory, "Snippets");
  string target = Path.Combine(Settings.ProjectDirectory, "Snippets");

  if (!Directory.Exists(target))
    Directory.CreateDirectory(target);

  IBuildStep step = new CopyDirectoryBuildStep<TeamBuildEngine>(this, source, target);

  // Allow the Sandcastle engine to create the normal build steps
  BuildStepCollection steps = base.CreateSteps();

  // Insert the new step before all other steps so it's executed first
  steps.Insert(0, step);

  return steps;
}

In the example above, the built-in CopyDirectoryBuildStep<TEngine> class is used to copy the snippets from the team's share into the local Snippets folder. If you need more control you can create a custom implementation of the IBuildStep interface or you can create a class that derives from the BuildStep<TEngine> base class.

Create DocProject Options

Although it's not required, most build engines would not be very useful without some options that can be set on a per-project basis. To add individual options to DocProjects and DocSites you must derive a class from DocProjectOptions or a derived type, such as SandcastleProjectOptions:
  1. Add a new class and name it, TeamProjectOptions.
  2. Derive the class from DaveSexton.DocProject.Sandcastle.SandcastleProjectOptions.
  3. Add a constructor that accepts a single DaveSexton.DocProject.IDocProject argument.
Example 5
public class TeamProjectOptions : SandcastleProjectOptions
{
  public TeamProjectOptions(IDocProject project)
   : base(project)
  {
  }
}

To use the options you must override the CreateProjectOptions method in the TeamBuildEngineProvider class:

Example 6
public override DocProjectOptions CreateProjectOptions(IDocProject project)
{
  return new TeamProjectOptions(project);
}

To make the TeamProjectOptions class useful it must contain some properties that can be configured for the associated DocProject or DocSite.

Since we've already added a property to the TeamBuildEngineProvider class where a user can specify the root path of the team's share, let's add a property to the TeamProjectOptions class that will allow users to add a relative path to the team's share, specific to their particular project:

Example 7
[Category("Team Options"), DisplayName("Shared Project Path"), 
Description("Relative path to the project's directory under the team's share.")]
public string TeamProjectDirectory
{
  get
  {
    return Project.Settings.Read("Team_TeamProjectDirectory");
  }
  set
  {
    Project.Settings["Team_TeamProjectDirectory"] = value;
  }
}

Notice how our option's value is stored in the DocProject or DocSite project file itself (Project.Settings).

Now that we've added a configurable option we should use its value in our build engine. Modify the CreateSteps method in the TeamBuildEngine class like this:

Example 8
protected override BuildStepCollection CreateSteps()
{
  // Get the provider's instance through the Project property
  TeamBuildEngineProvider provider = (TeamBuildEngineProvider) Project.Provider;

// ** Start new code here **
  TeamProjectOptions teamOptions = (TeamProjectOptions) Options;

  string source = provider.TeamSharedDirectory;
  string relativeSource = teamOptions.TeamProjectDirectory;

  if (!string.IsNullOrEmpty(relativeSource))
    source = Path.Combine(source, relativeSource);

  source = Path.Combine(source, "Snippets");
// ** End new code here **

  string target = Path.Combine(Settings.ProjectDirectory, "Snippets");

  if (!Directory.Exists(target))
    Directory.CreateDirectory(target);

  IBuildStep step = new CopyDirectoryBuildStep<TeamBuildEngine>(this, source, target);

  // Allow the Sandcastle engine to create the normal build steps
  BuildStepCollection steps = base.CreateSteps();

  // Insert the new step before all other steps so it's executed first
  steps.Insert(0, step);

  return steps;
}

As you can see in the example above, the root source directory is now a combination of the TeamSharedDirectory and TeamProjectDirectory properties. This allows users to configure a single UNC share for the team and then different relative project directories on a per-project basis.

Create Build Settings

As an internal service for build engines, the BuildSettings class provides a single place to encapsulate runtime settings that are used by the engine. This will allow more derived build engines to easily override paths and options at runtime and will greatly reduce the amount of code in your build engine class.

For this tutorial, let's take the new code that was added in Example 8 above and move it to a custom build settings class:
  1. Add a new class and name it, TeamSettings.
  2. Derive the class from DaveSexton.DocProject.Sandcastle.SandcastleSettings.
  3. Add a constructor that accepts a single DaveSexton.DocProject.Engine.IBuildEngine argument.
Example 9
public class TeamSettings : SandcastleSettings
{
  public TeamSettings(IBuildEngine engine)
    : base(engine)
  {
  }
}

To use the settings you must override the CreateBuildSettings method in the TeamBuildEngineProvider class:

Example 10
public override BuildSettings CreateBuildSettings(IBuildEngine engine)
{
  return new TeamSettings(engine);
}

Now let's add a property to the TeamSettings class that returns the source directory based on the current provider settings and the current user options. We'll also add another property that returns the relative code snippets directory and then one more property that returns the full path to the project's snippets:

Example 11
public virtual string TeamProjectPath
{
  get
  {
    // Get the provider's instance through the Project property
    TeamBuildEngineProvider provider = (TeamBuildEngineProvider) Project.Provider;

    // Get the option's instance through the Engine property
    TeamProjectOptions teamOptions = (TeamProjectOptions) Engine.Options;

    string source = provider.TeamSharedDirectory;
    string relativeSource = teamOptions.TeamProjectDirectory;

    if (!string.IsNullOrEmpty(relativeSource))
      source = Path.Combine(source, relativeSource);

    return source;
  }
}

public static readonly string DefaultRelativeSnippetsPath = "Snippets";

public virtual string RelativeSnippetsPath
{
  get
  {
    return DefaultRelativeSnippetsPath;
  }
}

public string SnippetsPath
{
  get
  {
    return Path.Combine(TeamProjectPath, RelativeSnippetsPath);
  }
}

The TeamProjectPath and RelativeSnippetsPath properties were made virtual so they can be overridden in derived classes. The SnippetsPath property does not need to be overridden since it simply returns the concatenation of the two virtual properties.

Now that we've encapsulated this logic let's update the CreateSteps method in the TeamBuildEngine class:

Example 12
protected override BuildStepCollection CreateSteps()
{
// ** Start new code here **
  TeamSettings settings = (TeamSettings) Settings;

  string source = settings.SnippetsPath;
  string target = Path.Combine(settings.ProjectDirectory, "Snippets");
// ** End new code here **

  if (!Directory.Exists(target))
    Directory.CreateDirectory(target);

  IBuildStep step = new CopyDirectoryBuildStep<TeamBuildEngine>(this, source, target);

  // Allow the Sandcastle engine to create the normal build steps
  BuildStepCollection steps = base.CreateSteps();

  // Insert the new step before all other steps so it's executed first
  steps.Insert(0, step);

  return steps;
}

Register the Provider

To get DocProject to notice our provider there are a few steps that we must take. If you make changes and rebuild the code then you must perform the second step again (GAC).

1) Sign the assembly:

  1. Right-mouse click the project node in Solution Explorer and select Properties
  2. Select the Signing tab.
  3. Check Sign the assembly.
  4. In the drop-down list select <New>.
  5. Name the key anything you'd like (e.g., Team.snk).
  6. Optionally, you can secure the key with a password.
  7. Click OK.
  8. Close the project properties.

2) Add the assembly to the Global Assembly Cache (GAC)

  1. Build the project and fix any errors before continuing.
  2. Open the Visual Studio Command Prompt, usually found under Start > Program Files > Microsoft Visual Studio 2005 > Visual Studio Tools
  3. Change the current directory to where your assembly is located (e.g, cd C:\Projects\TeamBuildEngineProvider\bin\Debug).
  4. Run the following command:
gacutil /i {assembly name}.dll

{assembly name}.dll should be replaced with the file name of the assembly built by your project.

When executed, the result should be, Assembly successfully added to the cache.

3) Add the provider to DocProject's configuration file

  1. Open Windows Explorer (Windows Key+E).
  2. Browse to DocProject's bin directory, commonly found at C:\Program Files\Dave Sexton\DocProject\bin.
  3. Open the DaveSexton.DocProject.dll.config file.
    1. Note: Do not confuse this file with the DocProject.exe.config file.
  4. Locate the configuration\addin\buildEngines element.
  5. Add a new <add> element to register your provider (see the example below).
  6. Save the changes.
Example 13
<add name="Team Share" 
     type="{namespace}.TeamBuildEngineProvider, {assembly name}, Version=1.0.0.0, Culture=neutral, PublicKeyToken={token}" />

Use a descriptive value for the name attribute since it will appear in the New Project Wizard's list of providers.

Also, replace {namespace} with your build engine provider's namespace and {assembly name} with your assembly's name.

You can find your assembly's public key {token}, which is a short hexadecimal string, by looking in the GAC:
  1. Open Windows Explorer (Windows Key+E).
  2. Browse to the GAC, commonly found at %windir%\assembly.
  3. Locate your assembly and you'll see the public key token next to it.
    1. Note: You can copy the key into your clipboard from the properties dialog (right-mouse click your assembly and select Properties).

Test the Provider

To test the provider we can start by creating a new DocProject. Then we'll create our source Snippets folder, add some files to be imported and build the new project.

Create a new DocProject

  1. Close all open instances of Visual Studio (this will ensure that your provider is loaded).
  2. Start a new instance of Visual Studio.
  3. Open an existing project to document or create an empty project for testing purposes:
    1. Go to File > New > Project....
    2. Select Visual C# and Class Library.
    3. Name the project whatever you'd like and click OK. The project is created.
    4. You can add code comments and enable Xml Documentation File output if you'd like, but it's not necessary for this test.
  4. Add a new DocProject that uses the new provider:
    1. Go to File > Add > New Project....
    2. Expand Visual C# > DocProject.
    3. Select the DocProject template.
    4. Enter any name that you'd like and click OK. DocProject's New Project Wizard will start.
    5. Select the Team Share build engine from the drop-down list and click Next.
    6. Use the wizard to configure the project however you'd like. When you're done, click Finish and the project will be created.
      1. Note: If you did not select at least one project to document on the last step then add a project reference or an external source now before continuing.
Now that we have a new DocProject that uses our build engine, let's configure the custom provider:
  1. Go to Tools > Options > DocProject > Engines.
  2. Select the Team Share provider from the drop-down list at the top.
  3. Locate the Team Options category.
  4. Set the Shared Directory option to an existing directory on your computer.
    1. For this test I recommend creating a new directory such as: C:\DocProjectTest\
This setting will be used by all DocProjects and DocSites that you create.

Let's configure our new DocProject by specifying a project-specific relative path under our team share:
  1. Go to Tools > Options > DocProject > Active Projects.
  2. Make sure the correct DocProject is selected in the drop-down list at the top if you have more than one in the active solution.
  3. Locate the Team Options category.
  4. Set the Shared Project Path option to: MyProject\
Click OK on the tools options dialog to commit the changes. Then save the project to commit the changes to the project file.

For this to work you must create a folder named, MyProject in the directory that you specified for the Shared Directory property, and a folder named, Snippets under that. Do that now.

For the sake of testing, also add some files into the Snippets directory so they will be imported when the project is built. You can add any file types since our code simply copies the entire directory, including subfolders.

Build the new DocProject

It's finally time to build the project and see our custom build engine in action. Go ahead and right-mouse click the new DocProject in Solution Explorer and select Build.

Note: You can cancel the help build (Ctrl+Break) after the first few steps if you want.

Once the build stops, view the build output window and look for Step 1 of N to see what information was provided. Then open Solution Explorer, select the new DocProject and click the Show All Files button at the top. A Snippets folder should appear in the project and it should contain all of the files that were in the source directory.

More Features

There are other features that you can take advantage of when creating a custom build engine provider, but they are beyond the scope of this tutorial:
  • Override the CreateNewProjectWizardStep method and inject custom wizard steps into DocProject's New Project Wizard.
  • Easily create custom toolbars and command buttons that appear in the Visual Studio IDE and the DocProject External UI.
  • Easily create custom menu commands and tool windows that appear in the Visual Studio IDE.
  • Add custom configuration sections to DocProject's configuration file specifically for your provider.
  • Indicate special project items that have special semantics to the build engine (the items are shown in the External UI's Build Items dialog automatically). The mere presence or absence of build items can control the type and status of a build and whether help is even built at all.
If you're interested in how to use these features check out the source code for the Sandcastle plug-in, DaveSexton.DocProject.Sandcastle.

Hopefully now you're familiar with the power of DocProject's extensibility and how you can use it in your organization to extend DocProject and Sandcastle. If you have any questions or new ideas about extensibility features please start a thread on the Discussions tab.

Last edited Mar 7, 2008 at 1:29 PM by davedev, version 10

Comments

No comments yet.