Extending NAnt with your own types
Last time I talked about how to nest existing elements in your custom NAnt tasks. Now that you've had a good look at that, and Jacob's custom task, there's only one other thing to cover in this subject: a new type.
Types end up looking a lot like a new task, but throwing this all together should be a lot of fun.
Last time, I explained how I envisioned this working; a neat little task that I would give a FileSet to, and a regular expression plus a replacement string on match. Considering this is a build process, you're going to be manipulating a lot of files with the same regex, that we have covered. But what if I want to apply more than one regex to each file that's processed; I'd like to pass a collection of regex elements to my AtalaFileRegex task along with my FileSet. For that you need the BuildElementCollection decorator on one of your field types, like this:
/// <summary>
/// A collection of regex/replace pairs that will be used on the file
/// </summary>
[BuildElementCollection("regexs", "regex", Required = true)]
[StringValidator(AllowEmpty = false)]
public AtalaRegexCollection Regexs
{
get { return _regexs; }
}
As Jacob explained, we're requiring this field exist in our task, and that it cannot be empty. But what about those first two arguments to BuildElementCollection? Those define the outer element's name, and the inner elements name, respectively. This will make your task have the following form:
<task someattribute=something someotherattribute=somethingelsle>
<regexs>
<regex ...>
<regex ...>
</regexs>
<fileset>
<include ...>
<exclude ...>
</fileset>
</task>
Now, all we're missing is the two types we said we're going to use in the BuildElementCollection.
First, we need our regex type so that we can work with it in our collection. For a type, you decorate the class itself, extend the Element type, and then decorate elements within the class as we've done before:
using System;
using NAnt.Core;
using NAnt.Core.Attributes;
namespace Atalasoft.NAnt.Types
{
/// <summary>
/// Represents an regex pattern
/// </summary>
[ElementName("regex")]
[Serializable()]
public class AtalaRegex : Element
{
#region Public Instance Constructors
/// <summary>
/// Initializes a new instance of the <see cref="AtalaRegex" />
/// class.
/// </summary>
public AtalaRegex()
{
}
#endregion Public Instance Constructors
I threw in the empty constructor since it's uninteresting.
Cool, now, we said we want the regex type to accept both a matching pattern and a replacement string, so let's get a couple of instance fields going on to hold those:
#region Private Instance Fields
private string _value;
private string _replace;
#endregion Private Instance Fields
And finally, our getters and setters, properly decorated:
#region Public Instance Properties
/// <summary>
/// The actual regex string
/// </summary>
[TaskAttribute("value")]
[StringValidator(AllowEmpty = false)]
public string Value
{
get { return _value; }
set { _value = value; }
}
/// <summary>
/// The replacement string on match
/// </summary>
[TaskAttribute("replace")]
[StringValidator(AllowEmpty = true)]
public string Replace
{
get { return _replace; }
set { _replace = value; }
}
#endregion Public Instance Properties
}
}
Notice on each, we've named the attribute (I called the regex string 'value' because I didn't want an element like <regex regex...). Also note, the replace string can be empty.
That's all there is to making your own, simple type. We'll move on to the more "complicated" part of the task at hand.
Second, realize that we told NAnt it's a collection, so when we make our regexs type it's going to need to extend the CollectionBase abstract class. You need nothing from the NAnt core. Let's set up the class, and the default constructors:
using System;
using System.Collections;
namespace Atalasoft.NAnt.Types
{
/// <summary>
/// Contains a strongly typed collection of <see cref="AtalaRegex" /> objects.
/// </summary>
[Serializable()]
public class AtalaRegexCollection : CollectionBase
{
#region Public Instance Constructors
/// <summary>
/// Initializes a new instance of the <see cref="AtalaRegexCollection"/> class.
/// </summary>
public AtalaRegexCollection()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AtalaRegexCollection"/> class
/// with the specified <see cref="AtalaRegexCollection"/> instance.
/// </summary>
public AtalaRegexCollection(AtalaRegexCollection value)
{
AddRange(value);
}
/// <summary>
/// Initializes a new instance of the <see cref="AtalaRegexCollection"/> class
/// with the specified array of <see cref="AtalaRegex"/> instances.
/// </summary>
public AtalaRegexCollection(AtalaRegex[] value)
{
AddRange(value);
}
#endregion Public Instance Constructors
From here, there's a lot of things to implement from the CollectionsBase class, and an enumerator. Suffice it to say, you're going to end up filling in a lot of stubs for insert, add, copyto, remove, etc. There's not much to cover here, it's a stong-typed collection.
Next time we'll put these things to work in our task.