Introduction
This article presents a custom control for Team Foundation Server's Work Item forms. The control is a textbox
that displays the result of a mathematical expression in which other numeric fields of the work item can participate.
Background
NOTE: This article is not intended to be a comprehensive guide on how to create/modify TFS work items or custom controls. For detailed information, please visit the links on the More Information section.
Team Foundation Server is a great platform for Application Lifetime Management. One of the features it provides is the ability to create and manage Work Items, which encapsulate single units of work.
TFS comes out of the box with standard work items defined for managing requirements, bugs, tasks, risks, and other artifacts commonly created when managing a software development project (these items are defined within two Project Templates, MSF for Agile Software Development and MSF for CMMI Process Improvement). However it is possible to define new work item types and to modify the existing work item types (and the same for the project templates).
Work items consist of Fields that store various kind of information; typical fields are the title, the name of the work item creator, the assigned priority, or the estimated implementation effort. In order to edit the information of these fields, each work item also defines the layout of a Form with various editable Controls (there are also non-editable controls, such as the work item change history). These forms are displayed by Team Explorer (a component of Visual Studio) when you create a new work item or when you edit and existing work item.
Team Explorer provides several ready made controls such as a simple one-line textbox, an HTML rich control, a date selector, a dropdown list, or a web browser. It is possible to create new types of controls and to use them in the TFS work item forms, and that's what you will see in this article.
So, What Is This?
In this article, you will meet ExpressionControl
, a TFS custom control that shows the result of calculating a mathematical expression. The nice part is that you can reference other fields of the work item in the expression, and if you change the value of one of these fields, the expression is re-evaluated and the value on the control is automatically updated. If you associate a work item field to the control, the calculated value is stored within the work item itself, therefore being available in work item queries and reports.
As a proof of concept, I provide a modified version of the Risk work item which comes with the MSF for CMMI Process Improvement project template. In this version, I have replaced the Severity field with Impact, which is a numeric field that can take any value. I have also added a Exposure field, which is automatically calculated by ExpressionControl
as probability multiplied by impact.
The control source code is provided as a Visual Studio 2010 solution which consists of one C# class library and one installer project. You can use the solution as a skeleton for creating your own TFS custom controls.
Installing and Using the Control
If you want to install and use ExpressionControl
right now, follow these steps:
- Pretty obvious, but ensure that you have Team Explorer 2010 installed.
- Download the installer from the downloads section.
- Run the installer. This will just copy two .dll files and one .wicc file (which is just a XML file) to one special subfolder inside the Visual Studio installation folder.
Now you can upload the modified Risk work item definition to the server. You can do that by using the witadmin
tool that comes with Team Explorer. Open a Visual Studio command prompt and execute:
witadmin importwitd /collection:(URL_of_your_TFS_server_and_collection)
/p:(name_of_the_team_project) /f:Risk.wit
Or, if you have TFS Power Tools installed on Visual Studio, select Tools - Process Editor - Work Item Types - Import WIT
on the Visual Studio menu.
Now try to create a new work item of type Risk. You will see that when you modify either the Probability or the Impact fields, the Exposure field is updated automatically:
More generally, the way of using the control in a work item is by defining a new control of type ExpressionControl
, and adding an attribute named Expression
whose value is the expression to be calculated. The expression can contain basic operations (+ - * /) and the operands can be either numbers, or field names enclosed in brackets, as in [Priority]
. For example, in the modified Risk definition we can find the following line:
<Control
FieldName = "Konamiman.TFS.Exposure"
Type = "ExpressionControl"
Expression = "[Probability]*[Impact]"
Label = "Exposure:"
LabelPosition = "Left"
ReadOnly = "True">
Associating a field to the control is not mandatory, but it is necessary if you want the calculated value to be persisted to the work item in the TFS database. For example, we can create a work item query that shows risks with priority, impact and exposure:
For the modified Risk work item, we have a backing field defined as follows:
<FIELD name="Exposure" refname="Konamiman.TFS.Exposure" type="Integer">
<HELPTEXT>Exposure of the risk, calculated as probability by impact.</HELPTEXT>
</FIELD>
You may want to define the field as double
instead of integer
, if you know that your calculation will give non-integer numbers. For example, you could have defined your expression as ([Probability]*[Impact])/100
.
In order to insert a new field and control on an existing work item, you need to download the work item definition file from the server. With the witadmin
tool, you would do:
witadmin exportwitd /collection:(URL_of_your_TFS_server_and_collection)
/p:(name_of_the_team_project) /n:(work_item_type_name) /f:(destination_file_name>
Or with TFS Power Tools, select Tools - Process Editor - Work Item Types - Export WIT
on the Visual Studio menu.
The Project
A TFS custom control is just a Windows Forms user control that implements the IWorkItemControl interface, accompanied by a descriptor file (.wicc) in XML format (see the links on the More Information section for details). Therefore the steps needed to create a custom control are the following:
- Create a new Visual Studio solution, of type Class Library.
- Add references to the
System.Windows.Forms
and System.Drawing
system assemblies.
- Add references to the following Team Explorer assemblies:
%ProgramFiles%\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\
Microsoft.TeamFoundation.WorkItemTracking.Controls.dll
%ProgramFiles%\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\
v2.0\Microsoft.TeamFoundation.WorkItemTracking.Client.dll
(Note: The root may be instead %ProgramFiles(x86)%
on your system.)
- Add a new item of type User Control, and make it implement
IWorkItemControl
.
- Add the appropriate controls on the User Control design surface.
- Add the code for the
IWorkItemControl
(a few more details on this later).
-
Add a new XML file, name it YourControlName.wicc (the name of the file will be the name that TFS will use to refer to your control) and set its compilation action to Content. The contents of the file must be as follows:
="1.0"
<CustomControl xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Assembly>YourClassLibraryFileName.dll</Assembly>
<FullClassName>YourControlFullClassName</FullClassName>
</CustomControl>
Once you have compiled your code, you need to copy the resulting .dll file, together with the .wicc file, to the following location:
%ProgramData%\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\10.0
And that's it, Team Explorer will recognize your control and display it, as long as you have declared it in a work item.
The Installer
Although deploying your control is as easy as copying two files to the TFS custom control folder as shown above, it is not a bad idea to have an installer project to do the job. Nick Hoggart explains how to create such project, however since I have done a couple more things than him, I will describe here the full process I have followed for creating the installer:
- Create a new project in the solution, of type Setup project.
- Open the File System Editor tab, and add a new folder of type Custom Folder, named CommonAppDataFolder. Set its
DefaultLocation
property to [CommonAppDataFolder]
.
- Under the
CommonAppData
folder, create the hierarchy for the folder Microsoft\Team Foundation\Work Item Tracking\Custom Controls\10.0, one folder at a time. See the following screenshot, borrowed from Hoggart's page itself:
- Right click on the 10.0 folder, and select Add - Project Output. Select Primary Output and Content Files from your class library project (optionally you can select Debug Symbols as well, this will be useful if you want to debug your code).
- On the Detected Dependencies folder of the setup project in Solution Explorer, select all the Microsoft.TeamFoundation.* dependencies that have been detected, right click and select Exclude, to avoid the installer copying them together with your library.
- On the User Interface Editor pane, remove the second and the third screen. These are the install location selection screen (not necessary, since files are always installed at a fixed location) and the install confirmation screen (useless if the install location selection screen has been removed). Unless of course you want to let the user choose if the install will be done for all users or only for the current user (I always set the InstallAllUser project property to
True
).
Now we will add a launch condition to avoid the installer to run if Team Explorer is not installed. This is strictly not necessary but will make our installer a bit neater.
- Open the Launch Conditions Editor tab.
- Right click on Search Target Machine, then select Add Registry Search.
- Select the registry search just added, open the propertieswindow, and set the properties as follows: Name = Team Explorer 2010, Property = TEAMEXPLORERINSTALLED, Root = vsdrrHKCU, RegKey = Software\Microsoft\VisualStudio\10.0_Config\InstalledProducts\Team Explorer. See the following screenshot:
- Right click on Launch Conditions, then select Add Launch Condition.
- Select the launch condition just added, open the properties window, and set the properties as follows: Name = Team Explorer 2010, Condition = TEAMEXPLORERINSTALLED, Message = Error: Team Explorer 2010 is not installed. See the following screenshot:
The last step would be to appropriately configure the setup project properties (Author, Description, URLs, and the like). You can pretty much adapt the provided setup project (and the class library project, for that matter) for your own use by changing "Konamiman" into your own name or company name.
The Code
Now I'll explain the key points of the control code itself. You can download the Visual Studio solution if you want to take a closer look.
The Expression Evaluator
First of all, I must say that I have not crafted my own expression evaluator. Instead, I have used the excellent Sebastien Ros' State of the Art Expression Evaluation, which provides all the functionality I needed and even more. I recommend you to take a look at Sebastien's article, but here are the basic points you need to know in order to understand how the evaluator is used in ExpressionControl
:
- The core of the evaluator is the
Expression
class. You pass a string
with the expression to be evaluated when you instantiate the class, for example:
var expression = new Expression("([Priority] * [Impact]) / 100");
- You use the
Evaluate
method of the Expression
object in order to trigger the expression evaluation. The method returns an object with the result of the evaluation (ExpressionControl
handles numbers only, but the Expression
class can handle strings, booleans and DateTimes
as well).
- The
Expression
class can handle parameters, that is, alphanumeric identifiers (enclosed in brackets) that are resolved dynamically when the expression is evaluated. When a parameter is found during the evaluation, an event named EvaluateParameter
is fired. This event gives the parameter name, and expects the parameter value to be set on one of the properties of the associated EventArgs
. For example:
Expression e = new Expression("2 * [Pi]");
e.EvaluateParameter += delegate(string name, ParameterArgs args)
{
if (name == "Pi") {
args.Result = 3.14;
}
};
var result = e.Evaluate();
The source code of the expression evaluator is embedded in the ExpressionControl
project itself, under the folder ExpressionEvaluator. The evaluator code depends on a library, Evaluant.Antlr.dll, which is referenced by the project (and of course must be copied together with the control library at install time).
The Control Code
The ExpressionControl
visual surface is just a single-line TextBox
used to display the results of the expression evaluation. It will be read-only or not, depending on how you configure the ReadOnly
property of control in the containing Work Item form (of course, it is recommended to configure it as read-only). Team Explorer creates an instance of the control when you create a new work item or when you select an existing work item for edition, provided that the control is declared in the work item form definition.
I will now enumerate the main members of the ExpressionControl
class, explaining the key points of each one so that you can get a general idea of how the control works.
- IWorkItemControl.Properties: This is a
StringDictionary
which contains all the attributes of the control, as declared in the XML element Control
on the work item form definition (all the XML attributes of the Control
element are included here). When this property is set, we retrieve the Expression
attribute and use it to instantiate an Expression
object, whose EvaluateParameter
event is assigned to a handling method (expression_EvaluateParameter
).
- IWorkItemControl.WorkItemDatasource: This is the work item which is being edited by the form that contains the control (although the property is declared as
Object
, the value is always of type WorkItem
). When this property is set, we assign a handler to the work item's FieldChanged
event (method OnFieldChanged
) so that we can appropriately re-evaluate the expression and update the control value if necessary.
- IWorkItemControl.FlushToDatasource: This method is invoked when the control value has to be backed up to the associated field on the work item (so the value is persisted together with the rest of the work item data in the TFS database). We simply check that the control is actually associated to a work item field (if not, we haven't where to persist) and that the field is of numeric type (string fields are accepted too, in this case the value is simply
ToString
-ized before it is set), then we set the new value for the field.
- GenerateNewTextboxValue: This method is invoked when the work item form is loaded (via the
IWorkItemControl.InvalidateDatasource
method) and when one of the fields referenced in the expression changes (via the OnFieldChanged
method). It simply invokes the Evaluate
method of the Expression
object and returns the result as a string
to be set on the TextBox
(the result is also cached on the calculationValue
class field, so that the FlushToDatasource
method can use it). The method takes care of all possible error conditions, returning an appropriate error message instead of a calculation result if necessary.
- expression_EvaluateParameter: This is invoked by the expression evaluator whenever it encounters a parameter in the expression. We assume that the parameter is a field name and query the work item for the field value. If the work item does not declare a field with such name, or if the field is not numeric, we throw an exception, which will be captured by the
GenerateNewTextboxValue
method and converted into an error message.
And that's pretty much it. There are a few more auxiliary members, but the core of the control is in the ones listed above.
Debugging
When I had the control finished, I found that it worked pretty well, except for one detail: when saving a work item that contained the control, Visual Studio would show me a "Object reference not set to an instance of an object" message box, otherwise saving the work item correctly. It turned out that the FieldChanged
event is fired when the work item is about to be saved, but with the Field
property of the WorkItemEventArgs
argument set to null
. I don't know if this is "bug or feature", but for what I see it seems that it is safe to just ignore the event when the Field
property is null
.
I would never have found the bug if I had not debugged the code. However you can't debug a TFS custom control directly as if it were an ordinary Visual Studio project (you can't "run" it). So, what can we do? MSDN has the answer (scroll down until you see the accepted answer). Just a note: you have to skip steps 10 and 11 (you can't "start" a class library project). For completeness, I paste here the procedure (the original refers to VS 2008, I have changed the references to VS 2010):
- Open up VS 2010 and load your project.
- Delete all breakpoints in your project.
- Right click on the project and go to the project properties.
- For the configuration, select Active (Debug).
- Under the enable debuggers section, check Enable unmanaged code debugging and enable the Visual Studio hosting process.
- Save.
- Right click on your project and click Clean.
- Right click on your project again and click Rebuild.
- In Windows Explorer, go to debug folder and copy the output (.dll, .exe., etc.) to the C:\Documents and Settings\All Users\Application Data\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\10.0 (this is where TF 2010 WI looks for the .dll and .wicc files).
- (Skip this!) On your toolbar with the drop down set to debug, click the arrow.
- (Skip this!) You will get a mini screen with your control on it. There should be a load button. Click on it and make sure you load the .dll/.exe from the path specified in step 9.
- Leave it open and don't close anything. Open up another instance of VS 2010.
- In Team Explorer, open the Work item type that contains your custom control (Add Work Item-> . . . . . ).
- Go back to the instance of VS 2010 in step 1 and go to Debug->Attach to Process.
- In the attach to Process Window, leave the Transport as Default and the Attach To field to have checked managed code and native code.
- In the Available processes window, you will see the process devenv.exe with an ID and the title of the Work Item you just created. Select that row and click on Attach.
- You will see some processing in the background as it is loading symbols. Once it is done, set your breakpoints accordingly.
- Go back to your 2nd instance of VS2010 (step 12) and perform what you need to perform on the Work item you created in step 13 to make sure the break point gets hit.
- Once your feel the breakpoint is hit, go back to the 1st instance of VS2010 (step 1 and step 17) and you will see the breakpoints you set hit. Now all you have to do is debug (sigh!).
More Information
If you want to learn more about TFS work item customization and about TFS custom control, take a look at these resources:
Final Note
This is my first TFS custom control, therefore the code may contain mistakes. If that's the case, or if you have improvement suggestions, please drop me a comment.
History
- 1 October, 2010: First version
- 6 October, 2010: Screenshots updated, now all the Visual Studio labels are displayed in English