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.