8

I'd like TFS 2010 to run a bit of custom code whenever a particular workflow transition happens. Is that possible?

I've found documentation about Custom Actions, which seem to be actions that can automatically trigger work item transitions (am I getting that right?) I also found Custom Activities, which are related to Builds. But nothing that serves this particular requirement - am I missing something?

Thanks for your help!

James Orr
  • 5,005
  • 7
  • 39
  • 63

2 Answers2

14

This is very doable.

It is so doable, that there are many ways to do it. One of my favorites is to make a server side plugin. (Note, this only works on TFS 2010)

These blog posts show the basics:

Here is some code that I have modified from my open source project TFS Aggregator:

public class WorkItemChangedEventHandler : ISubscriber
{       
    /// <summary>
    /// This is the one where all the magic starts.  Main() so to speak.
    /// </summary>
    public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext, NotificationType notificationType, object notificationEventArgs,
                                                out int statusCode, out string statusMessage, out ExceptionPropertyCollection properties)
    {
        statusCode = 0;
        properties = null;
        statusMessage = String.Empty;
        try
        {
            if (notificationType == NotificationType.Notification && notificationEventArgs is WorkItemChangedEvent)
            {
                // Change this object to be a type we can easily get into
                WorkItemChangedEvent ev = notificationEventArgs as WorkItemChangedEvent;
                // Connect to the setting file and load the location of the TFS server
                string tfsUri = TFSAggregatorSettings.TFSUri;
                // Connect to TFS so we are ready to get and send data.
                Store store = new Store(tfsUri);
                // Get the id of the work item that was just changed by the user.
                int workItemId = ev.CoreFields.IntegerFields[0].NewValue;
                // Download the work item so we can update it (if needed)
                WorkItem eventWorkItem = store.Access.GetWorkItem(workItemId);

                if ((string)(eventWorkItem.Fields["State"].Value) == "Done")
                    {
                        // If the estimated work was changed then revert it back.  
                        // We are in done and don't want to allow changes like that.
                        foreach (IntegerField integerField in ev.ChangedFields.IntegerFields)
                        {
                            if (integerField.Name == "Estimated Work")
                            {
                                eventWorkItem.Open();
                                eventWorkItem.Fields["Estimated Work"].Value = integerField.OldValue;
                                eventWorkItem.Save();
                            }
                        }
                    }
                }
            }

        }
        return EventNotificationStatus.ActionPermitted;
    }

    public string Name
    {
        get { return "SomeName"; }
    }

    public SubscriberPriority Priority
    {
        get { return SubscriberPriority.Normal; }
    }

    public WorkItemChangedEventHandler()
    {
        //DON"T ADD ANYTHING HERE UNLESS YOU REALLY KNOW WHAT YOU ARE DOING.
        //TFS DOES NOT LIKE CONSTRUCTORS HERE AND SEEMS TO FREEZE WHEN YOU TRY :(
    }

    public Type[] SubscribedTypes()
    {
        return new Type[1] { typeof(WorkItemChangedEvent) };
    }
}

/// <summary>
/// Singleton Used to access TFS Data.  This keeps us from connecting each and every time we get an update.
/// </summary>
public class Store
{
    private readonly string _tfsServerUrl;
    public Store(string tfsServerUrl)
    {
        _tfsServerUrl = tfsServerUrl;
    }

    private TFSAccess _access;
    public TFSAccess Access
    {
        get { return _access ?? (_access = new TFSAccess(_tfsServerUrl)); }
    }
}

/// <summary>
/// Don't use this class directly.  Use the StoreSingleton.
/// </summary>
public class TFSAccess
{
    private readonly WorkItemStore _store;
    public TFSAccess(string tfsUri)
    {
        TfsTeamProjectCollection tfs = new TfsTeamProjectCollection(new Uri(tfsUri));
        _store = (WorkItemStore)tfs.GetService(typeof(WorkItemStore));
    }

    public WorkItem GetWorkItem(int workItemId)
    {
        return _store.GetWorkItem(workItemId);
    }
}
Vaccano
  • 78,325
  • 149
  • 468
  • 850
  • 2
    One important thing to note is that this event is a notification (not a decision point). That means that there is nothing you can do to block it. You can only react to it. – Vaccano Mar 02 '11 at 00:08
  • @Vaccano - your [TFS Aggregator](http://tfsaggregator.codeplex.com/) project has been useful to our team. Just wanted to say thanks... I'm off to your project on codeplex now to spread the upvotes! – Saul Dolgin Feb 12 '13 at 11:56
  • @SaulDolgin - Glad to hear it has been helpful! – Vaccano Feb 12 '13 at 16:13
  • Is there a way to get the TFS URI from the context without needing a settings file and helper library? – Christopher Painter Feb 19 '14 at 14:36
  • var locationService = requestContext.GetService(); var uri = locationService.GetSelfReferenceUri(requestContext, locationService.GetDefaultAccessMapping(requestContext)); – Tamir Daniely Dec 21 '14 at 17:01
  • @Vaccano - I haven't been able to find where Store lives. Did you mean WorkItemStore? Or do you know in which assembly Store is defined? – Josh Dec 29 '15 at 14:22
  • @Josh - I have not done anything with this for years. But I did notice that if you scroll down in my example, the `Store` class is there. I looks like I made a wrapper class and called it `Store`. – Vaccano Dec 29 '15 at 21:56
0

Here is an example of my singleton pattern

public class TFSSingleton
{
    private static TFSSingleton _tFSSingletonInstance;
    private TfsTeamProjectCollection _teamProjectCollection;
    private  WorkItemStore _store;

    public static TFSSingleton Instance
    {
        get
        {
            if (_tFSSingletonInstance == null)
            {
                _tFSSingletonInstance = new TFSSingleton();
            }
            return _tFSSingletonInstance;
        }
    }

    public TfsTeamProjectCollection TeamProjectCollection
    {
        get { return _teamProjectCollection; }
    }

    public WorkItemStore RefreshedStore
    {
        get
        {
            _store.RefreshCache();
            return _store;
        }
    }

    public WorkItemStore Store
    {
        get { return _store; }
    }

    private TFSSingleton()
    {
        NetworkCredential networkCredential = new NetworkCredential("pivotalautomation", "*********", "***********");


        // Instantiate a reference to the TFS Project Collection
        _teamProjectCollection = new TfsTeamProjectCollection(new Uri("http://********:8080/tfs/**********"), networkCredential);
        _store = (WorkItemStore)_teamProjectCollection.GetService(typeof(WorkItemStore));
    }
}

and here is how it is referenced.

WorkItemTypeCollection workItemTypes = TFSSingleton.Instance.Store.Projects[projectName].WorkItemTypes;