Wednesday, July 29, 2009

Writing a Custom NAnt Task (Part 3)

Over the past couple of months, I have been working on writing the SchemaSpy Task for NAnt located on CodePlex.  When I began to create a few custom tasks for NAnt in the past, I found very little documentation about how to create one.  Much of the documentation surrounding the process how to create a custom task is based solely on examples without any supporting information on how to expand such.  While this works very well for basic things, there were some experiences I had to work through while writing the task for SchemaSpy that I would like to talk about here.  In this, the third of four parts, we'll be diverting away from the example created by part 1 and 2 and dive straight into examples from the SchemaSpy Task for NAnt code in order to examine how to create a custom element collection for your task.  In this post, we'll be reviewing the Schemas property of the SchemaSpy Task for NAnt.


Why Do I Need a Custom Element Collection?

For a very large number of tasks, basic attribute-based task properties should be enough.  However, if you find yourself where you need to allow a list of inputs into your task, you are stuck with two different options.  The first option is to simply create a basic property just like those described in the first two parts in this series in order to set a delimited string from the build file.  While this is easy to code and implement, it is not the most user friendly after the list grows in size.  The second option is to create a child element that represents a collection of elements.  This is a little bit more complex to develop; however, it provides a much better experience for the people who have to maintain the build file.  Unlike a simple decorated class property, a custom element collection requires things to be implemented in the code.


Creating Collection Items

The first item that we need to create is a simple object that will represent the children nodes of your custom element collections.  Looking at the code, this is represented by the Schema class in the Schema.cs file.  This class has to inherit from the NAnt.Core.Element class and be decorated with the ElementName attribute, the attribute is used to identify the name of the XML node in the build file.  In the example (see the code below), the name of the element will be "schema".  After the class is established, we are able to add decorated properties that will represent the different attributes for the element. 

   1:  [ElementName("schema")]
   2:  public class Schema : Element 
   3:  { 
   4:      /// <summary>
   5:      /// The name of the schema to analyze and document. 
   6:      /// </summary> 
   7:      [TaskAttribute("schemaName")]  
   8:      [StringValidator(AllowEmpty = false)]  
   9:      public string SchemaName { get; set; } 
  10:  }

Creating the Strongly-Typed Collection

The second item we need to add to our task is a strongly-typed collection to store instances of our Schema object we just created. Looking at the code, this is represented by the SchemaCollection class in the SchemaCollection.cs file.  Like many strongly-type collections created in .Net, this class inherits from the System.Collections.CollectionBase class.  Simply by implementing the abstract base class and filling in the code, the custom, strongly-typed collection of Schema objects will be created and ready to use.


Implementing the Collection

The third and fourth tasks left to add our collection of custom elements to our NAnt task is to update the task code itself.  Now that we have the collection and child elements created, we have to first create a private variable to hold a new instance of our collection object.  If the collection is not created and instantiated, NAnt will not be able to add items to the collection and throw an error. 

The last thing to do is to create a new decorated property to tell NAnt to access and expect the collection.  Unlike the other properties that we declared that describe node elements, we need to decorate a property with the BuildElementCollection attribute.  The attribute requires three parameters.  The first describes the name of the collection node.  The second describes the name of the children nodes. The last parameter describes the task options.  Once all of these items are set, the new code should look like the below:

   1:  /// <summary>   
   2:  /// Instantiates a default collection to add elements to.   
   3:  /// </summary>   
   4:  private SchemaCollection _schemaCollection = new SchemaCollection();   
   6:  /// <summary>   
   7:  /// Gets or sets the collection of schemas to use.  
   8:  /// </summary>   
   9:  [BuildElementCollection("schemas", "schema", Required = false)]  
  10:  public SchemaCollection Schemas   
  11:  {  
  12:       get { return _schemaCollection; }      
  13:       set { _schemaCollection = value; }  
  14:  }


A Look at the Build File

With everything completed, the code can be compiled and installed for NAnt to begin using it.  Once all of that is setup, you can access and update the build file to take advantage of the customer collection similar to the example below:

   1:  <example prop1="test">   
   2:      <schemas>   
   3:          <schema schemaName="dbo" />   
   4:          <schema schemaName="sys" />   
   5:      </schemas>   
   6:  </example>



In this section we reviewed the code implemented by the SchemaSpy Task for NAnt I wrote and placed out on CodePlex.  We looked at what it takes to implement a custom collection.  In the next and final installment of this series, we'll be looking at writing a custom task that is used to call an external application and look at what differences there are between it and the task we have been working with in the first two segments of this series.

kick it on DotNetKicks.comShout it

1 comment:

  1. Great post! You should definitely check out Project UppercuT. It's NAnt for automated builds to the maximum.