In praise of Sandcastle

Adventures with sandcastle

September 25, 2014 - 7 minute read -
code documentation

It’s been about 2 years since I first used Sandcastle, and the excellent Sandcastle Help File Builder. I was immediately impressed with the project, but my employer at the time claimed to have no use for documentation, so it remained something to play around with in my own time. I recently took another look at it, about a year or so since I last played around with it, and was very impressed to see the changes with the recent (May 2014) release of SHFB. Hereafter, whenever I refer to Sandcastle, I’m talking about the Sandcastle Help File Builder, not the underlying engine.

The biggest change was the Visual Studio integration. SFHB projects are now supported in Visual Studio (when the extension is installed), so your documentation projects can be included as 1st class members of your solution. As the documentation project is part of the solution, you can add your sources in the same manner as you would add a reference to another project in the solution.

The output is, as always, fantastic especially the web site. It’s highly configurable and supports maml for writing static pages, although I’ve not played around with this too much so far. It also includes support for Code Contracts, which I appreciate.

I’ve set up an example to demonstrate what I’m talking about. Sandcastle Project

A typical, if basic, example of XmlDoc on an extension method;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
namespace Example
{
    using System.Diagnostics.Contracts;
    using System.Text;
    using System.Xml;

    /// <summary>
    /// This class contains extension methods pertaining to the 
    /// <see cref="XmlDocument"/> class.
    /// </summary>
    public static class XmlDocumentExtensions
    {
        /// <summary>
        /// The method returns the <see cref="XmlDocument.InnerXml">InnerXml</see> 
        /// of the provided <see cref="XmlDocument"/> formatted with line feeds
        /// and indentations.
        /// </summary>
        /// <param name="xmlDocument">
        /// The document to format.
        /// </param>
        /// <returns>
        /// The contents to the document, formatted with line breaks and indentation.
        /// </returns>
        public static string ToIndented(this XmlDocument xmlDocument)
        {
            Contract.Requires(xmlDocument != null);
            Contract.Ensures(Contract.Result<string>() != null);
            var stringBuilder = new StringBuilder();
            var xmlSettings = new XmlWriterSettings
            {
                Indent = true,
                IndentChars = "  ",
                NewLineChars = "\r\n",
                NewLineHandling = NewLineHandling.Replace
            };
            using (var xmlWriter = XmlWriter.Create(stringBuilder, xmlSettings))
            {
                xmlDocument.Save(xmlWriter);
            }
            return stringBuilder.ToString();
        }
    }
}

We can also provide example code, to show the consumers of our types how to use them. We need to encode our angle brackets, so it get’s quite unwieldy for c# style generic examples or xml (well, we only really have to encode the left angle brackets).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
namespace Example
{
    using System.Diagnostics.Contracts;
    using System.Text;
    using System.Xml;

    /// <summary>
    /// This class contains extension methods pertaining to the 
    /// <see cref="XmlDocument"/> class.
    /// </summary>
    public static class XmlDocumentExtensions
    {
        /// <summary>
        /// The method returns the <see cref="XmlDocument.InnerXml">InnerXml</see>
        /// of the provided <see cref="XmlDocument"/> formatted with line feeds 
        /// and indentations.
        /// </summary>
        /// <param name="xmlDocument">
        /// The document to format.
        /// </param>
        /// <returns>
        /// The contents to the document, formatted with line breaks and indentation.
        /// </returns>
        /// <example>
        /// <code>
        /// var document = new XmlDocument();
        /// document.Load("&lt;Fella&gt;&lt;Name&gt;Fred&lt;/Name&gt;&lt;Age&gt;30&lt;/Age&gt;&lt;/Fella&gt;");
        /// Debug.Print(document.ToIndented());
        /// // prints:
        /// // &lt;Fella&gt;
        /// //     &lt;Name&gt;Fred&lt;/Name&gt;
        /// //     &lt;Age&gt;30&lt;/Age&gt;
        /// // &lt;/Fella&gt;
        /// </code>
        /// </example>
        public static string ToIndented(this XmlDocument xmlDocument)
        {
            Contract.Requires(xmlDocument != null);
            Contract.Ensures(Contract.Result<string>() != null);
            var stringBuilder = new StringBuilder();
            var xmlSettings = new XmlWriterSettings
            {
                Indent = true,
                IndentChars = "  ",
                NewLineChars = "\r\n",
                NewLineHandling = NewLineHandling.Replace
            };
            using (var xmlWriter = XmlWriter.Create(stringBuilder, xmlSettings))
            {
                xmlDocument.Save(xmlWriter);
            }
            return stringBuilder.ToString();
        }
    }
}

We can provide as many examples, with as many code blocks as we like. We can also specify the language of the example (C# is the default), and Sandcastle will highlight it for us accordingly. Another neat trick is, we can specify a source file and region name and sandcastle will pull the code example from there, allowing you to either use live code examples, or keep longer examples in a separate file. Here I’ve added a .cs file and a .vb file to an Examples folder and set their build action to none. Text files would work just as well, but this makes separating examples by language easier. We can write our code examples and surround them in regions, then let sandcastle draw them in.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
namespace Example
{
    using System.Diagnostics.Contracts;
    using System.Text;
    using System.Xml;

    /// <summary>
    /// This class contains extension methods pertaining to the 
    /// <see cref="XmlDocument"/> class.
    /// </summary>
    public static class XmlDocumentExtensions
    {
        /// <summary>
        /// The method returns the <see cref="XmlDocument.InnerXml">InnerXml</see>
        /// of the provided <see cref="XmlDocument"/> formatted with line feeds 
        /// and indentations.
        /// </summary>
        /// <param name="xmlDocument">
        /// The document to format.
        /// </param>
        /// <returns>
        /// The contents to the document, formatted with line breaks and indentation.
        /// </returns>
        /// <example>
        /// <code language="C#" source="..\Example\Examples\Examples.cs" region="ToIndentedExample" />
        /// <code language="VB" source="..\Example\Examples\Examples.vb" region="ToIndentedExample" />
        /// </example>
        public static string ToIndented(this XmlDocument xmlDocument)
        {
            Contract.Requires(xmlDocument != null);
            Contract.Ensures(Contract.Result<string>() != null);
            var stringBuilder = new StringBuilder();
            var xmlSettings = new XmlWriterSettings
            {
                Indent = true,
                IndentChars = "  ",
                NewLineChars = "\r\n",
                NewLineHandling = NewLineHandling.Replace
            };
            using (var xmlWriter = XmlWriter.Create(stringBuilder, xmlSettings))
            {
                xmlDocument.Save(xmlWriter);
            }
            return stringBuilder.ToString();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#region ToIndentedExample
var xmlDocument = new XmlDocument();
xmlDocument.Load("<Fella><Name>Fred</Name><FavouriteDrinks><Drink>Beer</Drink><Drink>Whisky</Drink><Drink>Tea</Drink></FavouriteDrinks></Fella>");
Debug.Print(xmlDocument.ToIndented());
// prints:
// <Fella>
//     <Name>Fred</Name>
//     <FavouriteDrinks>
//         <Drink>Beer</Drink>
//         <Drink>Whisky</Drink>
//         <Drink>Tea</Drink>
//     </FavouriteDrinks>
// </Fella>
#endregion
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#Region "ToIndentedExample"
Dim xmlDocument = New XmlDocument()
xmlDocument.Load("<Fella><Name>Fred</Name><FavouriteDrinks><Drink>Beer</Drink><Drink>Whisky</Drink><Drink>Tea</Drink></FavouriteDrinks></Fella>")
Debug.Print(xmlDocument.ToIndented())
' prints:
' <Fella>
'     <Name>Fred</Name>
'     <FavouriteDrinks>
'         <Drink>Beer</Drink>
'         <Drink>Whisky</Drink>
'         <Drink>Tea</Drink>
'     </FavouriteDrinks>
' </Fella>
#End Region

When you navigate through the web site that Sandcastle generates to the ToIndented method, you get the following page. Sandcastle documentation page