MSBuild Task Generator: Part 4. Implementing ITaskProperty and introducing a few new attributes.

Yesterday’s post was … well … boring.  But a lot of software is boring so this really isn’t a surprise.  Today’s will have a little more to it but still – we’re laying the foundation before talking about the more interesting things (the code generation) … but that starts soon.

 

So bear with me a little longer.

 

Today we’re implementing ITaskProperty in a class named MBFTaskProperty.

 

This is the class declaration and private data member:

 

namespace MBFTaskGenerator

{

      public class MBFTaskProperty : ITaskProperty

      {

            string m_Name;

            PropertyType m_Type;

            bool m_Required;

            string m_Default;

            string m_Range;

            string m_OneOf;

            bool m_IsArray;

      ...

      }

}

I would wager that from this you can figure out what most of the accessors look like so I won’t bore you.

 

But notice one thing – there is no System.Type member but there is an ITaskProperty member (Type) that returns a System.Type instance.

 

This is pretty easy to implement.  First you will recall the XSD was run through xsd.exe to create classes for serialization.  One of those classes (actually an enum) is PropertyType.  If you haven’t run the XSD through xsd.exe please do so now.  Refer to yesterday’s post for the specific command to execute.

 

So to get the type there is a two-step process:

 

1) figure out the base type

2) determine if it is an array type

 

So the pattern looks like this:

 

public Type Type

{

      get

      {

            Type t = null;

            switch (m_Type)

            {

                  case PropertyType.ITaskItem:

                        t = typeof(Microsoft.Build.Framework.ITaskItem);

                                    break;

                  ...

                  case PropertyType.SystemBoolean:

      t = typeof(System.Boolean);

                        break;

                  case PropertyType.SystemUInt64:

                        t = typeof(System.UInt64);

                        break;

                  default:

                        throw new ArgumentException(string.Format("Unknown type: {0}", m_Type), "Type");

            }

            if (this.IsArray)

            {

      t = System.Array.CreateInstance(t, 1).GetType();

            }

return t;

      }

}

 

Simple enough, right?  Switch on the enum, figure out the basic type, and if it is an array then create an array of that type and return the type.  I’m not sure if there is a better way to get the type of an array of a type from the original type (say that 5 times fast), but for prototype code this is a nice one-line hack.

 

Before we go further I want to introduce a few more members of the ITaskProperty interface.  Each of the following is an optional attribute you can apply to each Property element in the XML input to the code generator.

 

IsArray:  Boolean.  Whether or not to generate an array of the Type member.  The alternative was to have more types (Boolean, BooleanArray, etc).

 

Default:  String.  The default value.  The string will be converted to the target type.  If it can’t be then an exception will be thrown.  At build time if the task parameter is not defined then it will default to this value.  Otherwise .NET defaults apply (Boolean is false, Int* is 0, etc).

 

Range: String.  A numeric Range string.  You can provide a range (“1-10”), a single numeric (“42”) and any semi-colon separated collection (“1-10;42;50-75;99”).  When applied to a numeric type (System.Int32, for example) this will generate the code necessary to ensure that at build time the provided value is an acceptable value.  For example if you provided the range “1;2;4;8” and at build-time an attempt to assign “12” was made then an exception would be thrown.  Can only be applied to numeric types.  If a Default is specified then it must be a valid member of the range.

 

OneOf: String.  A string of values.  A semi-colon separated list of values that the build-time value must be one of.  This is only used for System.String types.  For example if the one of value is “Top;Bottom;Left;Right” then at build time only the string values “Top”, “Bottom”, “Left” or “Right” will be allowed.  Anything else will generate an exception.  If Default is specified it must be one of these.  If the Type is not System.String this will throw an exception at code-gen time.  OneOf  and IsArray are not compatible.  This could be changed easily but at the current time you can’t do this.

 

Required:  Boolean.  If this is “true” then the “Required” attribute will be applied to the property.  If it is false then it will not.  Defaults to false.

 

Example input (using Default and OneOf) and the generated output.  Notice that the code generator uses a utility class to validate the input instead of generating the validation routines everything.  The original prototype I wrote did put the validation everywhere.  This was more difficult to maintain and did not provide me any benefit.  However this change was on the primary reasons I opt’d for using interfaces and allowing the end-user to create their own code generators.

 

<Property Name="TestProperty" Type="System.String" Default="None" OneOf="None;OnlyInline;Any"/>

 

Generates the following:

 

#region TestProperty

private string m_TestProperty = "None";

       

public virtual string TestProperty

{

get

{

return this.m_TestProperty;

}

set

{

if (MBFTaskUtils.OneOf.IsOneOf("None;OnlyInline;Any", this.m_TestProperty))

{

this.m_TestProperty = value;

  return;

}

throw new System.ArgumentOutOfRangeException("Unexpected value type setting field \"TestProperty\". Expected one of" +

                        ": None;OnlyInline;Any");

}

}

#endregion

 

Here is another example, this time using Range.  Again notice how a utility class is used to perform the validation.  This reduces the amount of code gen that has to occur while still allowing for a flexible solution.

 

<Property Name="TestProperty" Type="System.Int32" Range="1,2,4,8,16"/>

 

Generates the following:

 

#region TestProperty

private int m_TestProperty;

       

public virtual int TestProperty

{

get

{

return this.m_TestProperty;

}

set

{

string stringRange = "1,2,4,8,16";

MBFTaskUtils.IntRangeCollection intRanges = MBFTaskUtils.IntRangeCollection.Parse(stringRange);

if (intRanges.Satisfies(value))

{

this.m_TestProperty = value;

return;

}

throw new System.ArgumentOutOfRangeException("Unexpected value type setting field \" TestProperty\". Expected value in " +

             "range: 1,2,4,8,16");

}

}

#endregion

 

Make sense?

 

If not then post a comment and let me know.  Tomorrow we’ll start talking about how we get from that XML input to the C# output.