How to build CHM / HTML Documentation with Sandcastle September CTP 2007 and NANT

The new Sandcastle CTP isn’t well documented at all and the documentation that can be found is useless, because the Sandcastle September CTP 2007 changed alot compared to old CTPs for that the documentation has been written. So I had to search about several hours to get my first CHM Documentation file automatically created with a NANT target. But step by step.

You first have to check some pre-requisites, before continueing.

  1. You have to install NANT version 0.85, if you don’t already have. (http://sourceforge.net/project/showfiles.php?group_id=31650)
  2. You have to install the Sandcastle September CTP 2007. (http://www.microsoft.com/downloads/details.aspx?FamilyId=E82EA71D-DA89-42EE-A715-696E3A4873B2&displaylang=en)
  3. After Installation check your environment variable DXROOT. It must point to the new installation directory of the Sandcastle September CTP. My environment still pointed to the old Sandcastle version, which leads to some failures.
  4. Open the commandline of windows (cmd.exe) and go into the installation directory of Sandcastle September CTP, subfolder “Examples\Sandcastle” and run “build_sandcastle.bat vs2005 test“. This creates reflection files for the complete .NET Framework. This takes probably a few minutes. So be patient.
  5. You have to install the Html Help Workshop (htmlhelp.exe – http://www.microsoft.com/downloads/details.aspx?familyid=00535334-c8a6-452f-9aa0-d597d16580cc&displaylang=en)

Now we can create a small example “Hello World” solution. To do this, simply create a new console application project with VS 2005 and store it to your preffered destination. Don’t forget to make the members public, otherwise you won’t get any result.

/// <summary>
/// This is a hello world example
/// </summary>
public class Program
{
    /// <summary>
    /// That's a small "Hello World" solution.
    /// </summary>
    /// <param name="args"></param>
    public static void Main(string[] args)
    {
        Console.WriteLine("Hello World.");
    }
}

As second you should not forget to specify a XML Documentation file in your Visual Studio project properties (Page Build). Because if not, you won’t see any custom comments.

Setting the XML Documentation file

Let us now create a simple NANT Script that executes the Sandcastle Creation Process.

<?xml version="1.0"?>
<project name="HelloWorld" default="build" basedir=".">

<property name="framework.dir" value="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727"/>
<property name="solution.file" value="HelloWorld" />
<property name="solution.output" value="HelloWorld\bin\debug\HelloWorld.exe"/>
<property name="solution.comments" value="HelloWorld\bin\debug\HelloWorld.xml"/>

<include buildfile="Sandcastle.include"/>

/// <!-- Build means, compile and document -->
<target name="build" depends="compile, Sandcastle.Doc" />

/// <!-- Compile sourcecode using msbuild -->
<target name="compile">

<exec program="${framework.dir}\msbuild.exe" commandline='"${solution.file}.sln" /t:Rebuild /p:Configuration=Debug'/>
<exec program="${framework.dir}\msbuild.exe" commandline='"${solution.file}.sln" /t:Rebuild /p:Configuration=Release'/>

</target>

</project>

As you can see, the only thing to do is to call the Sandcastle.Doc target and to set the correct solution properties. The Sandcastle.Include file does the rest for you. You only have to set the right path names into the Sandcastle.include.

The example solution, including the NANT Project can be downloaded here: HelloSandcastle Example!

And for the completness, below you can find the code of the Sandcastle.Include file.

Cheers
- Gerhard

kick it on DotNetKicks.com

Annotation: The NANT process will end with a minor failure that I could not resolve. The HHC process always ends up with a 1 which leads to this error. But the CHM File will be created, so you can ignore the failure.


<?xml version="1.0"?>
<project name="Sandcastle" default="build" basedir=".">

  <!-- Directories  -->
  <property name="hhc.exe" overwrite="false"     value="D:\Programme\HTML Help Workshop\hhc.exe" />
  <property name="sandcastle.dir"                value="D:\Programme\Sandcastle" />

  <!-- Executables -->
  <property name="sandcastle.mrefbuilder.exe"
     value="${sandcastle.dir}\productiontools\mrefbuilder.exe" />
  <property name="sandcastle.buildassembler.exe"
     value="${sandcastle.dir}\productiontools\buildassembler.exe" />
  <property name="sandcastle.xsltransform.exe"
     value="${sandcastle.dir}\productiontools\xsltransform.exe" />
  <property name="sandcastle.chmbuilder.exe"
     value="${sandcastle.dir}\productiontools\chmbuilder.exe" />

  <!-- Directories -->
  <property name="sandcastle.workingdir" value="Sandcastle" />
  <property name="sandcastle.html.dir"   value="${sandcastle.workingdir}\Html" />
  <property name="sandcastle.chm.dir"    value="${sandcastle.workingdir}\Chm" />
  <property name="sandcastle.chm.file"   value="${solution.file}"/>

  <!-- Transformations -->
  <property name="sandcastle.addoverloads.xsl"
     value="${sandcastle.dir}\ProductionTransforms\ApplyVsDocModel.xsl" />
  <property name="sandcastle.addfriendlyfilenames.xsl"
     value="${sandcastle.dir}\ProductionTransforms\AddFriendlyFilenames.xsl" />
  <property name="sandcastle.reflectiontomanifest.xsl"
     value="${sandcastle.dir}\ProductionTransforms\ReflectionToManifest.xsl" />
  <property name="sandcastle.createvstoc.xsl"
     value="${sandcastle.dir}\ProductionTransforms\createvstoc.xsl"/>

  <!--
    ******************************************************
    CREATE CHM DOCUMENTATION
    ******************************************************  -->
  <target name="Sandcastle.Doc">

    <!-- Check environment -->
    <fail if="${not file::exists(hhc.exe)}"
     message ="HTML Help Workshop not found at ${hhc.exe}"/>
    <fail if="${not file::exists(sandcastle.mrefbuilder.exe)}"
     message ="MRef Builder not found at ${sandcastle.mrefbuilder.exe}"/>
    <fail if="${not file::exists(sandcastle.buildassembler.exe)}"
     message ="Build Assembler not found at ${sandcastle.buildassembler.exe}"/>
    <fail if="${not file::exists(sandcastle.xsltransform.exe)}"
     message ="XSL Transform not found at ${sandcastle.xsltransform.exe}"/>
    <fail if="${not file::exists(sandcastle.chmbuilder.exe)}"
     message ="CHM Builder not found at ${sandcastle.chmbuilder.exe}"/>

    <!-- Specifies the DLL file that we want to document -->
    <property name="sandcastle.input.files"       value="..\${solution.output}"/>

    <!-- Delete working directory -->
    <delete dir="${sandcastle.workingdir}"/>

    <!-- Copy configuration file, and hard code references -->
    <copy file="${sandcastle.dir}/Presentation/vs2005/Configuration/Sandcastle.config"
          tofile="${sandcastle.workingdir}/Sandcastle.config">
      <filterchain>
        <replacestring from="%DXROOT%" to="${sandcastle.dir}\" />
        <replacestring from=".\comments.xml" to="..\${solution.comments}" />
        <replacestring from=".\Output\html" to=".\Html"/>
      </filterchain>
    </copy>

    <!-- Create HTML Output Environment -->
    <mkdir dir="${sandcastle.html.dir}" />

    <!-- Create CHM Output Environment -->
    <mkdir dir="${sandcastle.chm.dir}" />
    <copy todir="${sandcastle.chm.dir}">
      <fileset basedir="${sandcastle.dir}/Presentation/vs2005">
        <include name="Scripts\*.*"/>
        <include name="Icons\*.*"/>
        <include name="Styles\*.*"/>
      </fileset>
    </copy>

    <!-- Run MRefBuilder (introspection on assemblies) to create basic Reflection XML -->
    <exec program="${sandcastle.mrefbuilder.exe}" workingdir="${sandcastle.workingdir}">
      <arg value="${sandcastle.input.files}" />

      <!-- If you have dependencies 
      <arg value="/dep:&quot;D:\oracle\102\odp.net\bin\2.x\Oracle.DataAccess.dll&quot;"/>
      <arg value="/dep:&quot;C:\Program Files\Microsoft CE\v3.1\System.Data.SqlServerCe.dll&quot;"/>
      -->

      <arg value="/out:reflection.org1.xml" />
    </exec>

    <!-- Create final Reflection XML -->
    <!-- Regroup overloads -->
    <exec program="${sandcastle.xsltransform.exe}" workingdir="${sandcastle.workingdir}">
      <arg value="reflection.org1.xml" />
      <arg value="/xsl:&quot;${sandcastle.addoverloads.xsl}&quot;" />
      <arg value="/out:reflection.org2.xml" />
    </exec>

    <!-- Create filenames for html documents -->
    <exec program="${sandcastle.xsltransform.exe}" workingdir="${sandcastle.workingdir}">
      <arg value="reflection.org2.xml" />
      <arg value="/xsl:&quot;${sandcastle.addfriendlyfilenames.xsl}&quot;" />
      <arg value="/out:reflection.xml" />
    </exec>

    <!-- Create Manifest (list of Topics) -->
    <exec program="${sandcastle.xsltransform.exe}" workingdir="${sandcastle.workingdir}">
      <arg value="/xsl:&quot;${sandcastle.reflectiontomanifest.xsl}&quot;" />
      <arg value="reflection.xml" />
      <arg value="/out:manifest.xml" />
    </exec>

    <!-- Run BuildAssembler (create html topic files) -->
    <exec program="${sandcastle.buildassembler.exe}" workingdir="${sandcastle.workingdir}" >
      <arg value="manifest.xml" />
      <arg value="/config:Sandcastle.config" />
    </exec>

    <!-- Generate an intermediate Toc file that simulates the Whidbey TOC format. -->
    <exec program="${sandcastle.xsltransform.exe}" workingdir="${sandcastle.workingdir}">
      <arg value="/xsl:&quot;${sandcastle.createvstoc.xsl}&quot;" />
      <arg value="reflection.xml" />
      <arg value="/out:toc.xml" />
    </exec>

    <!-- Generate HHP File -->
    <exec program="${sandcastle.chmbuilder.exe}" workingdir="${sandcastle.workingdir}">
      <arg value="/project:${sandcastle.chm.file}" />
      <arg value="/html:Html" />
      <arg value="/lcid:1033" />
      <arg value="/toc:toc.xml" />
      <arg value="/out:Chm" />
    </exec>

    <!-- Generate CHM file -->
    <exec program="${hhc.exe}"
          commandline="${sandcastle.chm.file}.hhp"
          workingdir="${sandcastle.chm.dir}"
          failonerror="false"/>
  </target>
</project>

Set Operations with C# Generics

C# Generics are powerful, but there are not many generic classes beyond List and Dictionary. Especially for Set Operations Generics are missing.

That’s whay I created a small class for Set Operations called Set that contains the basic Set Operations, like Union, Intersect and Difference.

Here’s a small example how to use it:

public enum DataSource
{
    Database, XML, File, Stream, Http, Ftp
}

static void Main(string[] args)
{
    Set<DataSource> firstSet = new Set<DataSource>(
    DataSource.Database, DataSource.File, DataSource.XML);
    
    Set<DataSource> secondSet = new Set<DataSource>(
    DataSource.Http, DataSource.Ftp, DataSource.Stream, DataSource.XML, DataSource.File);
    
    Set<DataSource> union = firstSet.Union(secondSet);
    // result = Database, File, Xml, Http, Ftp, Stream
    
    Set<DataSource> intersect = firstSet.Intersect(secondSet);
    // result = Xml, File
    
    Set<DataSource> difference = firstSet.Difference(secondSet);
    // result = Http, Ftp, Stream
}

And below you’ll find the complete implementation.

Cheers
- Gerhard

kick it on DotNetKicks.com



using System.Collections;
using System.Collections.Generic;

namespace SetExample
{
    /// <summary>
    /// Set class
    /// </summary>
    /// <typeparam name="TElement"></typeparam>
    public class Set<TElement> : ICollection<TElement>
    {
        private List<TElement> internalList = new List<TElement>();
        
        /// <summary>
        /// Initializes a new instance of the <see cref="Set&lt;TElement&gt;"/> class.
        /// </summary>
        public Set()
        {
            
        }
        
        /// <summary>
        /// Initializes a new instance of the <see cref="Set&lt;TElement&gt;"/> class.
        /// </summary>
        /// <param name="elements">The elements.</param>
        public Set(params TElement[] elements)
        {
            AddRange(elements);
        }
        
        /// <summary>
        /// Adds the range.
        /// </summary>
        /// <param name="range">The range.</param>
        public void AddRange(IEnumerable<TElement> range)
        {
            foreach (TElement element in range)
            Add(element);
        }
        
        /// <summary>
        /// Unions the specified set.
        /// </summary>
        /// <param name="set">The set.</param>
        /// <returns></returns>
        public Set<TElement> Union(Set<TElement> set)
        {
            Set<TElement> result = new Set<TElement>();
            
            result.AddRange(this);
            result.AddRange(set);
            
            return result;
        }
        
        /// <summary>
        /// Intersects the specified set.
        /// </summary>
        /// <param name="set">The set.</param>
        public Set<TElement> Intersect(Set<TElement> set)
        {
            Set<TElement> result = new Set<TElement>();
            
            foreach (TElement element in set)
            if (Contains(element))
            result.Add(element);
            
            return result;
        }
        
        /// <summary>
        /// Differences the specified set.
        /// </summary>
        /// <param name="set">The set.</param>
        /// <returns></returns>
        public Set<TElement> Difference(Set<TElement> set)
        {
            Set<TElement> result = new Set<TElement>();
            
            foreach (TElement element in set)
            if (!Contains(element))
            result.Add(element);
            
            return result;
        }
        
        
        /// <summary>
        /// Gets or sets the <see cref="TElement"/> at the specified index.
        /// </summary>
        /// <value></value>
        public TElement this[int index]
        {
            get { return internalList[index]; }
            set { internalList[index] = value; }
        }
        
        #region ICollection<TElement> Members
        
        public void Add(TElement item)
        {
            if (!Contains(item))
            internalList.Add(item);
        }
        
        public void Clear()
        {
            internalList.Clear();
        }
        
        public bool Contains(TElement item)
        {
            return internalList.Contains(item);
        }
        
        public void CopyTo(TElement[] array, int arrayIndex)
        {
            internalList.CopyTo(array, arrayIndex);
        }
        
        public bool Remove(TElement item)
        {
            return internalList.Remove(item);
        }
        
        public int Count
        {
            get { return internalList.Count; }
        }
        
        public bool IsReadOnly
        {
            get { return false; }
        }
        
        #endregion
        
        #region IEnumerable<TElement> Members
        
        IEnumerator<TElement> IEnumerable<TElement>.GetEnumerator()
        {
            return internalList.GetEnumerator();
        }
        
        #endregion
        
        #region IEnumerable Members
        
        public IEnumerator GetEnumerator()
        {
            return internalList.GetEnumerator();
        }
        
        #endregion
    }
}

In-Code Profiling with C# and log4net

There are many tools that can be used for profiling software. These tools are a great enrichment for our job, but sometimes they are oversized. Sometimes, you need a small solution for a particular piece of code. If you find yourself at this point, I will show you a solution here.

The small piece of code, I want to show you, takes advantage of the using statement and the IDisposeable interface. The using statement can wonderful be used in order to encapsulate the code that you want to profile.

using (new ProfileRegion("RefreshContent"))
{
    #region Code you want to profile ....
}

The output is done with log4net and might look like:

2007-10-04 20:21:10,585 [5388] WARN : End Profiling: 4,986 seconds in region RefreshContent

The time will be logged as a warning when the given time span (default = 2 seconds) will be exceeded. Otherwise the profiling section will be logged as a debug message.

I think that is really a nice small class you can perfectly adopt in your project. What do you think?

Cheers
- Gerhard

kick it on DotNetKicks.com


using System;
using log4net;

namespace AdFactum.Utils
{
    /// <summary>
    /// This class describes a profiled region
    /// </summary>
    public class ProfileRegion : IDisposable
    {
        private readonly ILog log = LogManager.GetLogger(typeof(ProfileRegion));
        
        private DateTime startTime;
        private DateTime? endTime = null;
        
        private TimeSpan watermark = new TimeSpan(0, 0, 2);
        private string regionName;
        
        /// <summary>
        /// Gets the elapsed time.
        /// </summary>
        /// <value>The elapsed time.</value>
        public TimeSpan ElapsedTime
        {
            get
            {
                return (endTime == null) ?
                DateTime.Now - startTime :
                endTime.Value - startTime;
            }
        }
        
        /// <summary>
        /// Initializes a new instance of the <see cref="ProfileRegion"/> class.
        /// </summary>
        /// <param name="name">The name.</param>
        public ProfileRegion (string name)
        {
            regionName = name;
            startTime = DateTime.Now;
            
            if (log.IsDebugEnabled)
            log.Debug(string.Concat("Start Profiling: ", regionName));
        }
        
        /// <summary>
        /// Initializes a new instance of the <see cref="ProfileRegion"/> class.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="watermarkParam">The watermark param.</param>
        public ProfileRegion (string name, TimeSpan watermarkParam)
        :this(name)
        {
            watermark = watermarkParam;
        }
        
        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="ProfileRegion"/> is reclaimed by garbage collection.
        /// </summary>
        ~ProfileRegion()
        {
            Dispose(false);
        }
        
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        
        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposed"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        public void Dispose (bool disposed)
        {
            endTime = DateTime.Now;
            if (!disposed)
            log.Error(string.Concat("Region ", regionName, " not finalized by Dispose call!"));
            
            if (ElapsedTime < watermark)
            {
                if (log.IsDebugEnabled)
                log.Debug(string.Concat("End Profiling: ", ElapsedTime.TotalSeconds, " seconds in region ", regionName));
            }
            else
            {
                if (log.IsWarnEnabled)
                log.Warn(string.Concat("End Profiling: ", ElapsedTime.TotalSeconds, " seconds in region ", regionName));
            }
        }
    }
}

Posted in .NET, C#. 4 Comments »