Marius Schulz
Marius Schulz
Front End Engineer

Using the IndentedTextWriter Class to Output Hierarchically Structured Data

The BCL (base class library) of the .NET Framework provides a vast amount of functionality. Even though you might be familiar with large parts of it, chances are you don't know about some of the little goodies.

For me, one of those little helpers I didn't know about for quite some time is the IndentedTextWriter class hidden within the System.CodeDom.Compiler namespace. I'll show you how to use it to print a hierarchical list of items to the console.

#Modeling a To-Do List

Suppose we want to write a little application which displays a list of to-do items. Each to-do contains a description of the task and a list of optional sub-tasks. If an item has no sub-tasks, that list will be empty. (Please don't assign null to collections, ever!)

We could model a to-do item like this:

public class TodoItem
{
    public string Description { get; private set; }
    public IList<TodoItem> SubTasks { get; private set; }

    public TodoItem(string description)
    {
        Description = description;
        SubTasks = new List<TodoItem>();
    }
}

Here's a to-do list with some tasks that we can work with:

TodoItem[] todoList =
{
    new TodoItem("Get milk"),
    new TodoItem("Clean the house")
    {
        SubTasks =
        {
            new TodoItem("Living room"),
            new TodoItem("Bathrooms")
            {
                SubTasks =
                {
                    new TodoItem("Guest bathroom"),
                    new TodoItem("Family bathroom")
                }
            },
            new TodoItem("Bedroom")
        }
    },
    new TodoItem("Mow the lawn")
};

Let's now print the entire list of to-do items to the console while preserving the hierarchical nesting of sub-tasks through increasing indentation.

#Creating an IndentedTextWriter

The IndentedTextWriter defines the follwing two constructors:

  • IndentedTextWriter(TextWriter writer)
  • IndentedTextWriter(TextWriter writer, String tabString)

As you can see, both constructors require a TextWriter which holds the written output. You can also specify a tab string that's used to indent each line. If not specified otherwise, the tab string defaults to four spaces.

We'll use a StringWriter (which derives from the abstract TextWriter class) to hold the actual output. Since both the TextWriter and the IndentedTextWriter class implement IDisposable, we're going to embed them into two using statements:

public static void Main(string[] args)
{
    using (var output = new StringWriter())
    using (var writer = new IndentedTextWriter(output))
    {
        WriteToDoList(todoList, writer);
        Console.WriteLine(output);
    }
}

Remember to reference both the System.IO and the System.CodeDom.Compiler namespace. Also notice the usage of the two writers: The IndentedTextWriter is used to write the text, while the TextWriter is used to hold and retrieve the output.

#Recursively Writing Hierarchical Data

Finally, let's take a look at the WriteToDoList method:

private static void WriteToDoList(
    IEnumerable<TodoItem> todoItems,
    IndentedTextWriter writer
)
{
    foreach (var item in todoItems)
    {
        writer.WriteLine("- {0}", item.Description);

        if (item.SubTasks.Any())
        {
            writer.Indent++;
            WriteToDoList(item.SubTasks, writer);
            writer.Indent--;
        }
    }
}

The method iterates over all to-do items and prints each item to the console. Then, it checks if the to-do has any sub-tasks. If it does, it recursively calls itself and prints all sub-tasks at an increased indentation level. Here's what the output looks like:

- Get milk
- Clean the house
    - Living room
    - Bathrooms
        - Guest bathroom
        - Family bathroom
    - Bedroom
- Mow the lawn

While it's not the fanciest of classes in the BCL, the IndentedTextWriter might come in handy from time to time, e.g. when outputting log files, directory structures, or source code. Check out this little Gist for an overview of all code written for this post.