MSBuild Task Generator: Part 10. Generating properties and the OneOf constraint explained

Yesterday we generated the fields and initializers, so today let’s move on to the properties.

 

Let’s quickly look at an example input:

 

<?xml version="1.0" encoding="utf-8" ?>

<Tasks>

      <Task Namespace="ArrayTest" Class="Test1">

            <Property Name="StringWithOneOf" Type="System.String" OneOf="Value1;Value2;Value3"/>

      </Task>

</Tasks>

 

This should render the following (class and namespace removed):

 

#region StringWithOneOf

private string m_StringWithOneOf;

public virtual string StringWithOneOf

{

    get

    {

        return this.m_StringWithOneOf;

    }

    set

    {

        if (MBFTaskUtils.OneOf.IsOneOf("Value1;Value2;Value3", this.m_StringWithOneOf))

        {

            this.m_StringWithOneOf = value;

            return;

        }

        throw new System.ArgumentOutOfRangeException("Unexpected value type setting field \"StringWithOneOf\". Expected one of: Value1;V" +

                "alue2;Value3");

    }

}

#endregion

 

Notice a few things about this:

 

1) We are using the OneOf  attribute to specify a valid range of values.

2) We are using a utility classes (MBFTaskUtils) to perform the validation

 

The utility class looks exactly what you would expect.  We split the input (on the semi-colon) into a collection of values and perform a case-insensitive comparison of each value against the field value, returning a Boolean to indicate whether or not a match was found.

 

Since you’re quite familiar with the CodeDOM by now it should be no surprise how this works – here’s the function I used:

 

CodeStatement Generate_Call_IsOneOf (MBFTaskProperty tp, CodeMemberField field)

{

      CodeStatement[] assignAndReturn = new CodeStatement[] {

                  new CodeAssignStatement(new CodeFieldReferenceExpression(

                                                new CodeThisReferenceExpression(), field.Name),

                                          new CodePropertySetValueReferenceExpression()),

                                          new CodeMethodReturnStatement()

            };

      CodeTypeReferenceExpression OneOf_Type = new CodeTypeReferenceExpression(typeof(MBFTaskUtils.OneOf));

      CodeMethodReferenceExpression IsOneOf_Method = new CodeMethodReferenceExpression(

OneOf_Type,

"IsOneOf");

      CodeMethodInvokeExpression CallIsOneOf = new CodeMethodInvokeExpression(IsOneOf_Method,

                                                      new CodeExpression[] {

                                                            new CodePrimitiveExpression(tp.OneOfString),

                                                            new CodeFieldReferenceExpression(

new CodeThisReferenceExpression(),

field.Name)

                                                      }

);

      return new CodeConditionStatement(CallIsOneOf, assignAndReturn);

}

 

Piece of cake.  Render the assignment statement and return statement (the success condition), create the type reference and static method call with a few parameters, and finally drop it all into a conditional statement.

 

But where does the rendered exception throw come from?  That comes from the caller of this method.  A stripped down version of that function is this:

 

CodeStatementCollection GenerateSetStatement_AssignToValue(MBFTaskProperty tp, CodeMemberField field)

{

      CodeStatementCollection statements = new CodeStatementCollection();

      if (tp.OneOfString != null && tp.OneOfString.Length > 0)

      {

            statements.Add(Generate_Call_IsOneOf(tp, field));

            string expMsg = string.Format(

                        "Unexpected value type setting field \"{0}\". Expected one of: {1}",

                        tp.Name,

                        tp.OneOfString);

            statements.Add(Generate_Throw_OutOfRange(expMsg));

      }

      else

      {

            // field = value

            CodeExpression left = new CodeFieldReferenceExpression(

new CodeThisReferenceExpression(), field.Name);

            CodeExpression right = new CodePropertySetValueReferenceExpression();

            statements.Add(new CodeAssignStatement(left, right));

      }

      return statements;

}

If we’re rendering a constraint (e.g. OneOf) then do that, otherwise render a simple assignment.

 

And for the curious Generate_Throw_OutOfRange looks like:

 

CodeStatement Generate_Throw_OutOfRange(string msg)

{

      return new CodeThrowExceptionStatement(

                        new CodeObjectCreateExpression(typeof(ArgumentOutOfRangeException),

                        new CodePrimitiveExpression(msg)));

}

 

No surprises there.  Although this does introduce a few new classes.  CodeThrowExceptionStatement does what it sounds like.  It generates a “throw” statement.  CodeObjectCreateExpression too, is what it sounds like.  It generates a heap allocation (e.g. “new”) of an object.

 

So that is the framework for rendering properties as well as rendering a constraint.  The other constraints (i.e. Range) are rendered the same way.  There is a utility class whose static method is called, it returns a Boolean, and either the assignment is done or an ArgumentOutOfRange exception is thrown.  The implementation of Range is more interesting then the CodeDOM support for rendering it’s usage, so tomorrow we’ll look at that.

 

No warranty expressed or implied.  It works on my machine, if it works on yours let me know.  Don’t forget the GDN workspace is available too.