TFS 2010 – Customizing Build Information (Part 2)

My previous post introduced the basics of how the tracking participant may be used to create custom build information through the activity context. However, as pointed out at the end of the article, the best practice for creating custom activities in Workflow 4.0 is to do so through composition, meaning that you should derive from Activity or Activity<T> . In compliance with this model we supply activities that allow you to perform the same custom information creation through activity composition rather than from code. The intention of this article is to lead you through how you might make this type of functionality available, as well as introduce you to composing custom build information into your own build processes.

Tracking Custom Build Information using Activity Composition

As you may recall, we previously introduced a custom tracking record, BuildInformationRecord<T> , for sending instructions to the tracking participant to create custom information. In order to keep things generic, we want to allow users of our activity to have the same flexibility with our new activity that they have when interacting directly with the activity context. Since our activity will need to perform the interaction with the activity context on behalf of the workflow writer, we need to create a custom CodeActivity, which may be defined similarly to the class below.

 
 public sealed class WriteBuildInformation<T> : CodeActivity
{
    public WriteBuildInformation()
    {
    }

    [RequiredArgument]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public InArgument<T> Value { get; set; }

    protected override void Execute(CodeActivityContext context)
    {
        context.Track(new BuildInformationRecord<T>() { Value = Value.Get(context) });
    }
}

As you can see, our activity is simply a thin wrapper around the code we previously explored. However, there are a couple of advantages I’d like to point out when using this approach over the strict code approach.

  1. Since execution is very short lived, the tracking participant gets a chance to receive the record and take action once the method completes.
  2. Gives consumers of the activity a chance to incorporate custom information by building activities through composition in either XAML or code.

This activity should only be considered a utility activity that serves as the base for special purpose activities. The reason I propose limited use of this activity directly is because the XAML for it can get relatively tedious fairly quickly. For example, if we step back and model our example of writing a build message in XAML using this activity, we will end up with something very similar to the XAML below.

 <my:WriteBuildInformation x:TypeArguments="my:BuildMessage" xmlns:my="https://mycustomschema.company.com" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <my:WriteBuildInformation.Value>
    [New BuildMessage() With {.Importance = BuildMessageImportance.Normal, .Message = "This is a custom message" }]
  </my:WriteBuildInformation.Value>
</my:WriteBuildInformation>

Judging by the XAML, the verbosity could be greatly reduced if we didn’t need to specify the x:TypeArguments or construct the Value parameter with a visual basic expression; this is where activity composition comes in handy. We can provide any number of specialized wrapper activities that simplify the usage of this activity for particular information types. Once again, for the sake of consistency, we will revisit the BuildMessage for an example. What we would ultimately like to provide for users of our activity library is a single, specialized activity for writing build messages. Users should be able to set the Importance and the Message as arguments to the activity, just as they would when constructing the XAML above. Given this small set of requirements, we might end up with an activity that looks similar to the following:

 
 public sealed class WriteBuildMessage : Activity
{
    public WriteBuildMessage()
    {
        base.Implementation = () => CreateBody();
    }

    [RequiredArgument]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public InArgument<String> Message { get; set; }

    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public InArgument<BuildMessageImportance> Importance { get; set; }
 
    Activity CreateBody()
    {
        return new WriteBuildInformation<BuildMessage>
        {
            Value = new InArgument<BuildMessage>(ctx => new BuildMessage() { Message = Message.Get(ctx), Importance = Importance.Get(ctx) }),
        };
    }
}

There a few things I would like to point out about the code above, just in case not everyone is familiar with lambda syntax and activity composition in general.

  • The single line in the constructor is one of the most important lines. The way composed activities work is you provide a Func<Activity> , exposed as the property Implementation, that returns the private implementation of your activity (synonymous to the implementation of a method or function in code). If you forget to set this, the activity will not produce an error and will produce no output.
  • The InArgument<T> type has a few constructors. In this particular scenario we want to construct an object at runtime based on the incoming arguments. The syntax we must use in this case is the constructor that takes a Func<ActivityContext, T> so we can utilize the activity context handed to us by the runtime to extract the values of the arguments for our BuildMessage. Since Workflow 4.0 is very new and still in beta, I plan on covering general topics regarding activity design and best practices we learned through our use of the framework in TFS 2010 in coming posts. If you don’t quite understand what I’m talking about just be patient, for soon you will be a Workflow Master (probably not, but you’ll understand it better than you do now)!

Now that we have our specialized activity for writing build messages, we can express the equivalent XAML we saw earlier with the following:

 <my:WriteBuildMessage xmlns:my="https://mycustomschema.company.com" Importance="[BuildMessageImportance.Normal]" Message="This is a custom message" /> 

As you can see the syntax is much more concise than the previous example. It also takes the burden off the user of your activity to create an instance of the appropriate type and use initializer-syntax to set the property values. Although this simplifies the XAML you should probably use good judgment when writing these specialized activities to keep your library to a reasonable size. You will find the following activities exposed in the library that shipped with TFS 2010 Beta 2:

  • WriteBuildError
  • WriteBuildMessage
  • WriteBuildWarning

Using these activities you can, you guessed it, write messages, warnings, and errors. These particular information types are special in that they are well understood by our build report, however, and will show up with warning or error icons next to the message automatically when viewing the build log. Jason Prickett should be making a follow-up post soon describing how you can extend the build report to display your custom information. I’ll be sure to notify everyone once this happens so we can bring all of the customizations together and show off the entire scenario.

After writing these past 2 articles I have come to the realization that it would probably be very helpful to go over Workflow 4.0 in general so everyone can get up to speed on these topics. My next few posts will focus on development of activities and design practices with the 4.0 APIs, so keep reading for more in-depth explanations of the underlying framework.

Comments

  • Anonymous
    December 21, 2009
    Here's the follow-up post that Patrick promised I would make: http://blogs.msdn.com/jpricket/archive/2009/12/21/tfs-2010-displaying-custom-build-information-in-visual-studio.aspx Thanks, Jason

  • Anonymous
    November 17, 2010
    Is there an option to write message to the build log within the working thread of an async activity?

  • Anonymous
    April 11, 2011
    In public sealed class WriteBuildInformation<T>:CodeActivity context.Track(new BuildInformationRecord<T>() { Value = Value.Get(context) }); does not compile, it says Error 5 Argument 1: cannot convert from 'MyLib.Activities.WriteBuildInformation<T>.BuildInformationRecord<T>' to 'System.Activities.Tracking.CustomTrackingRecord'

  • Anonymous
    April 11, 2011
    Brandon: You need to make BuildInformationRecord extend from CustomTrackingRecord. I omitted some of the code details for clarity.