Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Integrating list of custom types in TFS Build Template

0.00/5 (No votes)
19 May 2014 1  
Integrating list of custom types into the TFS build template

Introduction

In this article I will show how to integrate list of custom objects with the TFS Build template. I am going to assume you have developed a custom TFS Build activity before, and have an understanding of build template and definition, if not, please refer to the resources at the end of this post for further clarification.

If your build is executing a list of similar tasks, making them into a list will often be a more flexible option and make your build definition easier to configure. I have gone through some hoops trying to get this to work, so hopefully this will help someone out there facing the same problem. The end result is shown below:

I have three NServiceBus services defined, where their properties can be modified directly in the build definition once they are expanded, or by using the custom editor.

Background

TFS Build supports basic primitive types customization, for example, we used to deploy services by customizing the template using bools and strings as follow:

While this works fine, it is not very flexible. If we add more services we will have to modify the build template, add the necessary code to the custom activity to handle the new services, and recompile the activity, modify the definition, etc. It involves a number of repetitive steps. This is one of the main motivation for converting this into a list so we can better manage them.

I looked through the default template, the closest functionality I can find is similar to "Configurations to Build" or that of adding Automated Tests to run.

I wanted to see how Microsoft was implementing that functionality. taking a cue from a post by Rory Primrose, I used ILSpy to dig through the workflow activities. After much trials and errors, I eventually found PlatformConfigurationList inside of the Microsoft.TeamFoundation.Build.Workflow.Acitivities.dll assembly. After studying the code in PlatformConfigurationList and tracing through the referenced objects, I have a general idea of what is required to implement what I wanted, which I will attempt to explain in the next section completed with the necessary codes.

The code

This is what I wanted to achieve, I will point out the circled section when appropriate:

First, I have a Service object, that represent a single NServiceBus service that we need to deploy, its properties are directly editable once its expanded in the build definition.

using System;
using System.ComponentModel;

namespace MyWorkflowActivities
{
    [Serializable]
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class Service
    {
        [Browsable(true)]
        [RefreshProperties(System.ComponentModel.RefreshProperties.All)]
        public bool Deploy { get; set; }

        [Browsable(true)]
        [RefreshProperties(System.ComponentModel.RefreshProperties.All)]
        public string Server { get; set; }

        [Browsable(true)]
        [RefreshProperties(System.ComponentModel.RefreshProperties.All)]
        public string TargetFolder { get; set; }

        [Browsable(true)]
        [RefreshProperties(System.ComponentModel.RefreshProperties.All)]
        public string ServiceName { get; set; }

        [Browsable(true)]
        [RefreshProperties(System.ComponentModel.RefreshProperties.All)]
        public string Description { get; set; }

        [Browsable(false)]
        public override string ToString()
        {
            return string.Format("{0}@{1}, deploy: {2}",
                this.ServiceName,
                this.Server,
                this.Deploy);
        }
    }
}

Then I have a ServiceList inheriting from BindingList<T>, this is so I can easily bind the services to be displayed in the DataGridView in my custom editor. ServiceList implements ICustomTypeDescriptor, this is needed so once expanded in the build definition, the list of services will be displayed instead of the properties of the BindingList<T>. ServiceList is the object that will be used in the build template and in my activity.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace MyWorkflowActivities
{
    [Serializable]
    [TypeConverter(typeof(ServiceListConverter))]
    public class ServiceList : BindingList<Service>, ICustomTypeDescriptor
    {
        public ServiceList()
        {
        }

        public ServiceList(IList<Service> services)
            : base(services)
        {
        }

        [Browsable(false)]
        public override string ToString()
        {
            var c = this.Count(x => x.Deploy);
            return string.Format("{0}/{1} services are set to deploy", c, this.Count);
        }

        public AttributeCollection GetAttributes()
        {
            return System.ComponentModel.TypeDescriptor.GetAttributes(this, true);
        }

        public string GetClassName()
        {
            return System.ComponentModel.TypeDescriptor.GetClassName(this, true);
        }

        public string GetComponentName()
        {
            return System.ComponentModel.TypeDescriptor.GetComponentName(this, true);
        }

        public TypeConverter GetConverter()
        {
            return System.ComponentModel.TypeDescriptor.GetConverter(this, true);
        }

        public EventDescriptor GetDefaultEvent()
        {
            return System.ComponentModel.TypeDescriptor.GetDefaultEvent(this, true);
        }

        public PropertyDescriptor GetDefaultProperty()
        {
            return System.ComponentModel.TypeDescriptor.GetDefaultProperty(this, true);
        }

        public object GetEditor(Type editorBaseType)
        {
            return System.ComponentModel.TypeDescriptor.GetEditor(this, editorBaseType, true);
        }

        public EventDescriptorCollection GetEvents(Attribute[] attributes)
        {
            return System.ComponentModel.TypeDescriptor.GetEvents(this, attributes, true);
        }

        public EventDescriptorCollection GetEvents()
        {
            return System.ComponentModel.TypeDescriptor.GetEvents(this, true);
        }

        public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
        {
            return this.GetProperties();
        }

        public PropertyDescriptorCollection GetProperties()
        {
            System.ComponentModel.PropertyDescriptorCollection propertyDescriptorCollection = new System.ComponentModel.PropertyDescriptorCollection(null);
            for (int i = 0; i < this.Count; i++)
                propertyDescriptorCollection.Add(new ServicePropertyDescriptor(this, i));
            return propertyDescriptorCollection;
        }

        public object GetPropertyOwner(PropertyDescriptor pd)
        {
            return this;
        }
    }
}

ServiceList has a ServiceListConverter that inherits from ExpandableObjectConverter. The interesting bit here is ConvertTo(), you can use the object (string in this case) returned to display a status message about the list, as circled in red in the image above.

using System;
using System.ComponentModel;
using System.Linq;

namespace MyWorkflowActivities
{
    public class ServiceListConverter : ExpandableObjectConverter
    {
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
        }

        public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string) && value is ServiceList)
            {
                ServiceList list = (ServiceList)value;
                var c = list.Count(x => x.Deploy);
                return string.Format("{0}/{1} service(s) are set to deploy", c, list.Count);
            }

            return base.ConvertTo(context, culture, value, destinationType);
        }
    }
}

ServicePropertyDescriptor is used in ServiceList.GetProperties() where eventually the list of services is displayed when expanded. We can also display a status message for each service, as circled in blue in the image above.

using System;
using System.ComponentModel;

namespace MyWorkflowActivities
{
    public class ServicePropertyDescriptor : PropertyDescriptor
    {
        private ServiceList serviceList;
        private Service service;
        private int index;

        public ServicePropertyDescriptor(ServiceList serviceList, int index) :
            base(string.Format("{0}{1}. {2}",
                (index + 1) > 9 ? string.Empty : "0",
                (index + 1).ToString(System.Globalization.CultureInfo.InvariantCulture),
                serviceList[index].ServiceName), new System.Attribute[0])
        {
            this.serviceList = serviceList;
            this.index = index;
            this.service = this.serviceList[this.index];
        }

        public override bool CanResetValue(object component)
        {
            return false;
        }

        public override Type ComponentType
        {
            get { return this.PropertyType; ; }
        }

        public override object GetValue(object component)
        {
            return this.service;
        }

        public override bool IsReadOnly
        {
            get { return false; }
        }

        public override Type PropertyType
        {
            get { return this.service.GetType(); }
        }

        public override void ResetValue(object component)
        {
            throw new NotImplementedException();
        }

        public override void SetValue(object component, object value)
        {
            this.service = (Service)value;
            this.serviceList[this.index] = this.service;
        }

        public override bool ShouldSerializeValue(object component)
        {
            return false;
        }
    }
}

Then comes the UITypeEditor, this acts as a middle man between TFS Build and our custom editor. Whenever we open our custom editor, the existing data from the build definition is read and pass into our custom editor. When we are done editing in the editor, it sends the data back to be saved.

using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace MyWorkflowActivities
{
    public class SvcEditor : UITypeEditor
    {
        public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
        {
            if (provider != null)
            {
                IWindowsFormsEditorService editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));

                if (editorService == null)
                {
                    return value;
                }

                var serviceList = value as ServiceList;
                using (SvcDialog dialog = new SvcDialog())
                {
                    dialog.ServiceList = serviceList;
                    if (editorService.ShowDialog(dialog) == DialogResult.OK)
                    {
                        value = new ServiceList(dialog.ServiceList);
                    }
                }
            }

            return value;
        }

        public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
        {
            return UITypeEditorEditStyle.Modal;
        }
    }
}

Speaking of the custom editor, it is really just a simple Windows Form resided in the same namespace:

Its code behind:

using System;
using System.Windows.Forms;

namespace MyWorkflowActivities
{
    public partial class SvcDialog : Form
    {
        public ServiceList ServiceList { get; set; }

        public DataGridView DataGridView
        {
            get { return this.dgServices; }
        }

        public SvcDialog()
        {
            InitializeComponent();
        }

        private void btnAdd_Click(object sender, EventArgs e)
        {
            this.ServiceList.Add(new Service { TargetFolder = @"Infrastructure" });
        }

        private void btnRemove_Click(object sender, EventArgs e)
        {
            if (this.dgServices.SelectedRows.Count > 0)
                this.ServiceList.Remove((Service)this.dgServices.SelectedRows[0].DataBoundItem);
        }

        private void btnOK_Click(object sender, EventArgs e)
        {
            this.DialogResult = System.Windows.Forms.DialogResult.OK;
            this.Close();
        }

        private void SvcDialog_Load(object sender, EventArgs e)
        {
            this.dgServices.DataSource = this.ServiceList;
        }

        private void btnUp_Click(object sender, EventArgs e)
        {
            if (this.dgServices.SelectedRows.Count == 0) return;
            var row = this.dgServices.SelectedRows[0];
            var prevIdx = row.Index;
            if (prevIdx - 1 < 0) return;
            var item = this.ServiceList[prevIdx];
            this.ServiceList.Remove(item);
            this.ServiceList.Insert(prevIdx - 1, item);
            this.dgServices.Rows[prevIdx - 1].Selected = true;
        }

        private void btnDown_Click(object sender, EventArgs e)
        {
            if (this.dgServices.SelectedRows.Count == 0) return;
            var row = this.dgServices.SelectedRows[0];
            var prevIdx = row.Index;
            if (prevIdx + 1 >= this.ServiceList.Count) return;
            var item = this.ServiceList[prevIdx];
            this.ServiceList.Remove(item);
            this.ServiceList.Insert(prevIdx + 1, item);
            this.dgServices.Rows[prevIdx + 1].Selected = true;
        }
    }
}

Finally, we glue everything together in the build template, I am going to assume you have worked with build template before and only show the relevant portions here:

First, import our assembly, you will also have to make sure the build can access your assembly, if you are not sure how to do that, please take a look at some of the referenced resources:

xmlns:ldbw="clr-namespace:MyWorkflowActivities;assembly=MyWorkflowActivities"

Create a property that will hold the list of the added/modified services.

  
<x:Members>
    ...
    <x:property name="NServiceBusServices" type="InArgument(ldbw:ServiceList)">
    </x:property> 
</x:Members>

Hook up our custom editor for the NServiceBusServices property:

  
<this:Process.Metadata>
    <mtbw:ProcessParameterMetadataCollection>
    ...
    <mtbw:ProcessParameterMetadata Category="Services Configuration" Description="Configure NServiceBus Services" DisplayName="NServiceBus Services" Editor="MyWorkflowActivities.SvcEditor, MyWorkflowActivities" ParameterName="NServiceBusServices" />  

Then call our custom activity as part of the build.

<Sequence DisplayName="">
    <ldbw:SvcDeployAsync DisplayName="Service Deployment" 
    ServiceList="[NServiceBusServices]" 
</Sequence> 

Note that I haven't included the code to the custom activity since it will be similar to any of the activities you might have developed, where it will have a public property InArgment<ServiceList> to read in the ServiceList specified in the template, then we do whatever we need to do once we have the list of services.

And voila, that's it! Sit back and enjoy the awesome custom editor.

Points of Interest

The following post really helped me, please read them if anything is not clear here:

History

5/19/2014 - initial post.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here