Create custom work item control for TFS Web Access 2010 (TWA)

by Ewald Hofman 10. August 2010 05:06

For the customization of the work item controls for the client are many examples. There is even a codeplex project that hosts some examples. However if you want to provide a custom work item control for Web Access, it turns out that there is only little information. In this post I will describe what I did to create the custom Web Access control.

Thanks to Guneet Umra of Avanade and Serkan Inci of Microsoft to help me out on this.

For this example I will show you how you can create a textbox that has a blue background, but you can make it as fancy as you like.

  1. Open Visual Studio and create a new Class library called WebControl.
  2. Add the references to
    1. C:\Windows\assembly\GAC_MSIL\Microsoft.TeamFoundation.WebAccess.Controls\10.0.0.0__b03f5f7f11d50a3a\Microsoft.TeamFoundation.WebAccess.Controls.dll
    2. C:\Windows\assembly\GAC_MSIL\Microsoft.TeamFoundation.WebAccess.WorkItemTracking\10.0.0.0__b03f5f7f11d50a3a\Microsoft.TeamFoundation.WebAccess.WorkItemTracking.dll
    3. C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.WorkItemTracking.Client.dll
    4. C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\Microsoft.TeamFoundation.WorkItemTracking.Controls.dll

      Or you can add the following lines to your project file:

          <Reference Include="Microsoft.TeamFoundation.WorkItemTracking.Client, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
            <SpecificVersion>False</SpecificVersion>
            <Private>False</Private>
          </Reference>
          <Reference Include="Microsoft.TeamFoundation.WorkItemTracking.Controls, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
            <SpecificVersion>False</SpecificVersion>
            <Private>False</Private>
          </Reference>
          <Reference Include="Microsoft.TeamFoundation.WebAccess.WorkItemTracking, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
            <SpecificVersion>False</SpecificVersion>
          </Reference>
          <Reference Include="Microsoft.TeamFoundation.WebAccess.Controls, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
            <SpecificVersion>False</SpecificVersion>
          </Reference>

  3. Create a wicc file, named BlueTextbox.wicc (for more information the wicc file, see http://msdn.microsoft.com/en-us/library/bb286959.aspx)
  4. Set the Build Action (in the properties window) of this file to Content (so it is included in the setup project that will be created later)
  5. Add the following contents to the wicc file

    <?xml version="1.0" encoding="utf-8" ?>
    <CustomControl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
      <Assembly>WebControl.dll</Assembly>
      <FullClassName>WebControl.BlueTextbox</FullClassName>
    </CustomControl>
  6. Rename the Class1.cs file to BlueTextbox.cs
  7. Paste the following contents to the file

    using System;
    using System.Drawing;
    using Microsoft.TeamFoundation.WebAccess.WorkItemTracking.Controls;
    using System.Web.UI;
    using System.Web.UI.WebControls;

    namespace WebControl
    {
        public class BlueTextbox : BaseWorkItemWebControl
        {
            public BlueTextbox()
                : base(HtmlTextWriterTag.Div)
            {

            }

            /// <summary>
            ///  The textbox with the blue background
            /// </summary>
            public TextBox TextBox { get; set; }

            /// <summary>
            /// The value in the work item
            /// </summary>
            public string FieldValue
            {
                get
                {
                    // When it has a value, return that value
                    if (HasValidField && Field.Value != null)
                        return Field.Value.ToString();

                    // Else return the empty string
                    return string.Empty;
                }
                set
                {
                    // When the field is value and is editable
                    if (HasValidField && Field.FieldDefinition.IsEditable)
                        // And it is changed
                        if ((Field.Value != null || value != null)
                            && (Field.Value == null || Field.Value.ToString() != value))
                        {
                            // Set the value in the work item
                            base.OnBeforeUpdateDatasource(EventArgs.Empty);
                            Field.Value = value;
                            base.OnAfterUpdateDatasource(EventArgs.Empty);
                        }
                }
            }

            /// <summary>
            /// The value of the textbox
            /// </summary>
            public string Value
            {
                get
                {
                    EnsureInnerControls();

                    return TextBox.Text;
                }
                set
                {
                    EnsureInnerControls();

                    TextBox.Text = value;
                }
            }

            /// <summary>
            /// Build the control
            /// </summary>
            public override void InitializeControl()
            {
                base.InitializeControl();

                // Create the TextBox
                EnsureInnerControls();

                // Save the value when it is changed
                TextBox.TextChanged += delegate { FlushToDatasource(); };
            }

            /// <summary>
            /// Add the textbox
            /// </summary>
            public void EnsureInnerControls()
            {
                if (TextBox != null)
                    return;

                TextBox = new TextBox { BackColor = Color.Blue };

                base.Controls.Clear();
                base.Controls.Add(TextBox);
            }

            /// <summary>
            /// THe underlying data is changed. Change the contents of the TextBox accordingly
            /// </summary>
            public override void InvalidateDatasource()
            {
                base.InvalidateDatasource();

                // If the field is valid
                if (HasValidField)
                {
                    // And it has a value
                    if (Field.Value != null)
                    {
                        // change the contents of the TextBox
                        Value = Field.Value.ToString();
                    }
                    else
                    {
                        // else clear the value
                        Clear();
                    }
                }
                else
                {
                    // If the field is not valid, then also clear the contents of the TextBox
                    Clear();
                }

                // Ensure the control is shows correctly
                ResetStyles();
            }

            /// <summary>
            /// Ensure that the control is correctly shows (width + enabled)
            /// </summary>
            private void ResetStyles()
            {
                Width = Unit.Percentage(100);
                Enabled = !ControlReadOnly && !EditorReadOnly;
            }

            public override void FlushToDatasource()
            {
                base.FlushToDatasource();

                FieldValue = Value;
            }

        }
    }
  8. To distribute this control, the easiest way is to create a setup project. Since the location is different for x86 and x64 the easiest way is to create two setup projects.
  9. First create the x64 setup project: Add a new project of type Setup Project with the name WebControlSetup.x64 (you can find the project type in the Add New Project dialog under Other Project Types –> Setup and Deployment –> Visual Studio Installer)

    image
  10. Remove the User’s Desktop and the User’s Programs Menu folders
  11. Click on the project in the Solution Explorer and open the properties window. Change the TargetPlatform to x64
  12. Change the DefaultLocation of the Application Folder to [ProgramFiles64Folder]\Microsoft Team Foundation Server 2010\Application Tier\Web Access\Web\App_Data\CustomControls
  13. Right click on the right pane to add the Primary output of the WebControl project

    image 
  14. When you add the primary output also all the Microsoft.TeamFoundation dll’s are added. Those dll’s should not be installed however

    image
  15. You can remove those by opening up the Detected Dependencies, select those dll’s, right click to open the context menu and choose Exclude. You will see the dll’s will be removed from the File System window

    image
  16. Now also add the Content of the WebControl project
  17. Repeat these steps to create a x86 setup project, but now use the target folder “[ProgramFilesFolder]\Microsoft Team Foundation Server 2010\Application Tier\Web Access\Web\App_Data\CustomControls”
  18. You can now build the WebControl solution, and install the appropriate msi on the TFS server.
  19. Now change the work item type to make use of the new control. You can find information how to modify work item types at http://msdn.microsoft.com/en-us/library/ms243849.aspx
  20. When you change the FieldControl of the Title field in the FORMS section to BlueTextbox,

        <FORM>
          <Layout>
            <Group>
              <Column PercentWidth="80">
                <Control FieldName="System.Title" Type="BlueTextbox" Label="&amp;Title:" LabelPosition="Left" />
              </Column>
    you will see the following when you create a new task

    image
  21. There is a nice little feature in TFS to have two separate layouts for the Windows and the Web UI: You can add the attribute Target=”Windows” or Target=”Web” to the Layouts node to show two different layouts.

        <FORM>
          <Layout Target="WinForms">
            <Group>
              <Column PercentWidth="80">
                <Control Type="FieldControl" FieldName="System.Title" Label="&amp;Title:" LabelPosition="Left" />
              </Column>


          <Layout Target="Web">
            <Group>
              <Column PercentWidth="80">
                <Control Type="BlueTextbox" FieldName="System.Title" Label="&amp;Title:" LabelPosition="Left" />
              </Column>

Tags:

Work items | VSTS 2010 | TSWA

How to use WCF to subscribe to the TFS 2010 Event Service [rolling up hours]

by Ewald Hofman 2. August 2010 06:43

There is a lot of information on the web around the TFS Event Service, but that is mainly around ASMX and TFS 2008. I am using Visual Studio 2010, TFS 2010 and WCF. To be able to subscribe to the created event, the easiest is to have a local instance of TFS 2010 running on your machine.

The purpose of my WCF service will be to rollup the hours of the children into the hour fields (estimate, remaining work and completed work) of the (grand)parents. The result will be a framework on which you can build further as it is not smart enough yet to handle all the other link types (like bugs that are assigned to test cases etc.)

This article assumes that you have basic knowledge of WCF.

The steps that are involved into this exercise are:

  1. Create a new Visual Studio WCF Service Application
  2. Create the interface for the rollup Service
  3. Specify the web.config
  4. Subscribe the WCF servcie to the TFS WorkItemChanged event
  5. Write logic in the service to retrieve the changed work item
  6. Update the hours of the parent work item
  7. Deploy the web service to IIS
  8. Test and debug the service

Create a new Visual Studio WCF Service Application

Open Visual Studio 2010 and create a new project of the type WCF Service Application, which is called EventService. Cleanup the proposed situation by removing all methods in the IService1 and the Service1 files and the CompositeType. Then change the name of the files to RollupService and IRollupService (and check whether the name of the class and interface have changed too). We have now an empty WCF Service Application container in which we can add the logic.

Create the interface for the rollup Service

To be able to consume the event, we need to decorate the interface with the ServiceContract attribute and we need to add a new operation Notify that has two string arguments. In short your IRollupService will look like this:

using System.ServiceModel;

namespace EventService
{

    [ServiceContract(Namespace="http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03")]
    public interface IRollupService
    {

        [OperationContract(Action = http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify)]
        [XmlSerializerFormat(Style = OperationFormatStyle.Document)]
        void Notify(string eventXml, string tfsIdentityXml);

    }

}

Next implement the Notify method in the RollupService class as well.

namespace EventService
{
    public class RollupService : IRollupService
    {

        void IRollupService.Notify(string eventXml, string tfsIdentityXml)
        {
            // Add logic
        }

    }
}

 

Specify the web.config

In WCF the web.config is very important and it took me hours to find the correct contents of the config to finally hook up to the TFS 2010 system.

Here is the list of issues I had to solve:

Problem HTTP code 415: Cannot process the message because the content type 'application/soap+xml; charset=utf-8' was not the expected type 'text/xml; charset=utf-8'
Issue Because TFS 2010 is now using SOAP 1.2, you need to use wsHttpBinding.
Solution Change the basicHttpBinding to wsHttpBinding

Thanks to Mattias Sköldin his post  http://mskold.blogspot.com/2010/02/upgrading-tfs-event-subscriptions-to.html
   
Problem System.Web.Services.Protocols.SoapException: The message could not be processed. This is most likely because the action 'http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify' is incorrect or because the message contains an invalid or expired security context token or because there is a mismatch between bindings. The security context token would be invalid if the service aborted the channel due to inactivity. To prevent the service from aborting idle sessions prematurely increase the Receive timeout on the service endpoint's binding.
Issue You have to change the security of the binding to None
Solution Add a new bindig configuration (in the example see the EventServiceBinding) where you set the Security to None

Finally I was able to execute the notification with the following web.config

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="EventServiceBinding">
          <security mode="None" />
        </binding>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="EventServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service behaviorConfiguration="EventServiceBehavior" name="EventService.RollupService">
        <endpoint address="" binding="wsHttpBinding" bindingConfiguration="EventServiceBinding"
          contract="EventService.IRollupService" />
      </service>
    </services>
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>

</configuration>

Publish the web service to IIS

In Visual Studio right click on the project in the solution explorer, and find the Publish command. When you start the command, you get a dialog to enter the information where you want to publish this WCF Service to.

To exclude any issues with the interference with other web application, I always tend to use a new web site for this (in this example I used port 1001). I use the following information.

image 

Subscribe the WCF servcie to the TFS WorkItemChanged event

In the folder "C:\Program Files\Microsoft Team Foundation Server 2010\Tools” you can find the application bisubscribe with which you can subscribe your events. To register the service to be executed on every change of a work item execute the following command.

"C:\Program Files\Microsoft Team Foundation Server 2010\Tools\bissubscribe" /eventType WorkItemChangedEvent /address http://localhost:1001/RollupService.svc /collection http://localhost:8080/tfs/DefaultCollection

Write logic in the service to retrieve the changed work item

When you build the project and attach to the correct w3wp process, you can debug what is actually send to your service. it turns out that TFS sends this information:

eventXml
<?xml version="1.0" encoding="utf-16"?>
<WorkItemChangedEvent xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <PortfolioProject>MyTeamProject</PortfolioProject>
  <ProjectNodeId>f0fedf0c-3c7f-47e1-b144-ba85fc9765ba</ProjectNodeId>
  <AreaPath>\MyTeamProject</AreaPath>
  <Title>MyTeamProject Work Item Changed: Task 109 - MyNewTitle</Title>
  <WorkItemTitle>k</WorkItemTitle>
  <Subscriber>[DefaultCollection]\Project Collection Service Accounts</Subscriber>
  <ChangerSid>S-1-5-21-1482476501-2139871995-682003330-85089</ChangerSid>
  <DisplayUrl>http://localhost:8080/tfs/web/wi.aspx?pcguid=64925f8d-7df7-4369-a922-6e44d69d781f&amp;id=109</DisplayUrl>
  <TimeZone>W. Europe Daylight Time</TimeZone>
  <TimeZoneOffset>+02:00:00</TimeZoneOffset>
  <ChangeType>Change</ChangeType>
  <CoreFields>
    <IntegerFields>
      <Field>
        <Name>ID</Name>
        <ReferenceName>System.Id</ReferenceName>
        <OldValue>109</OldValue>
        <NewValue>109</NewValue>
      </Field>
      <Field>
        <Name>Rev</Name>
        <ReferenceName>System.Rev</ReferenceName>
        <OldValue>12</OldValue>
        <NewValue>13</NewValue>
      </Field>
      <Field>
        <Name>Area ID</Name>
        <ReferenceName>System.AreaId</ReferenceName>
        <OldValue>203</OldValue>
        <NewValue>203</NewValue>
      </Field>
    </IntegerFields>
    <StringFields>
      <Field>
        <Name>Work Item Type</Name>
        <ReferenceName>System.WorkItemType</ReferenceName>
        <OldValue>Task</OldValue>
        <NewValue>Task</NewValue>
      </Field>
      <Field>
        <Name>Title</Name>
        <ReferenceName>System.Title</ReferenceName>
        <OldValue>MyOldTitle</OldValue>
        <NewValue>MyNewTitle</NewValue>
      </Field>
      <Field>
        <Name>Area Path</Name>
        <ReferenceName>System.AreaPath</ReferenceName>
        <OldValue>\MyTeamProject</OldValue>
        <NewValue>\MyTeamProject</NewValue>
      </Field>
      <Field>
        <Name>State</Name>
        <ReferenceName>System.State</ReferenceName>
        <OldValue>Proposed</OldValue>
        <NewValue>Proposed</NewValue>
      </Field>
      <Field>
        <Name>Reason</Name>
        <ReferenceName>System.Reason</ReferenceName>
        <OldValue>New</OldValue>
        <NewValue>New</NewValue>
      </Field>
      <Field>
        <Name>Assigned To</Name>
        <ReferenceName>System.AssignedTo</ReferenceName>
        <OldValue>Ewald Hofman</OldValue>
        <NewValue>Ewald Hofman</NewValue>
      </Field>
      <Field>
        <Name>Changed By</Name>
        <ReferenceName>System.ChangedBy</ReferenceName>
        <OldValue>Ewald Hofman</OldValue>
        <NewValue>Ewald Hofman</NewValue>
      </Field>
      <Field>
        <Name>Created By</Name>
        <ReferenceName>System.CreatedBy</ReferenceName>
        <OldValue>Ewald Hofman</OldValue>
        <NewValue>Ewald Hofman</NewValue>
      </Field>
      <Field>
        <Name>Changed Date</Name>
        <ReferenceName>System.ChangedDate</ReferenceName>
        <OldValue>30-7-2010 13:39:12</OldValue>
        <NewValue>30-7-2010 13:56:49</NewValue>
      </Field>
      <Field>
        <Name>Created Date</Name>
        <ReferenceName>System.CreatedDate</ReferenceName>
        <OldValue>30-7-2010 11:42:24</OldValue>
        <NewValue>30-7-2010 11:42:24</NewValue>
      </Field>
      <Field>
        <Name>Authorized As</Name>
        <ReferenceName>System.AuthorizedAs</ReferenceName>
        <OldValue>Ewald Hofman</OldValue>
        <NewValue>Ewald Hofman</NewValue>
      </Field>
      <Field>
        <Name>Iteration Path</Name>
        <ReferenceName>System.IterationPath</ReferenceName>
        <OldValue>\MyTeamProject</OldValue>
        <NewValue>\MyTeamProject</NewValue>
      </Field>
    </StringFields>
  </CoreFields>
  <ChangedFields>
    <IntegerFields />
    <StringFields>
      <Field>
        <Name>Title</Name>
        <ReferenceName>System.Title</ReferenceName>
        <OldValue>MyOldTitle</OldValue>
        <NewValue>MyNewTitle</NewValue>
      </Field>
      <Field>
        <Name>Original Estimate</Name>
        <ReferenceName>Microsoft.VSTS.Scheduling.OriginalEstimate</ReferenceName>
        <OldValue>3</OldValue>
        <NewValue>6</NewValue>
      </Field>
      <Field>
        <Name>Remaining Work</Name>
        <ReferenceName>Microsoft.VSTS.Scheduling.RemainingWork</ReferenceName>
        <OldValue>3</OldValue>
        <NewValue>1</NewValue>
      </Field>
      <Field>
        <Name>Completed Work</Name>
        <ReferenceName>Microsoft.VSTS.Scheduling.CompletedWork</ReferenceName>
        <OldValue>0</OldValue>
        <NewValue>3</NewValue>
      </Field>
    </StringFields>
  </ChangedFields>
</WorkItemChangedEvent>
tfsIdentityXml
<TeamFoundationServer url="http://localhost:8080/tfs/DefaultCollection/Services/v3.0/LocationService.asmx" />

Based on the information that is in those arguments it is possible to see the changes of the effort fields (estimate, remaining and completed) from the current work item. You can use a simple xQuery to get the required information. To be able to get the work item, we need:

  1. the url to the TFS Server, which we can find in the tfsIdentityXml
  2. the parent work item, which we can find via the links of the changed work item. The eventXml has the id of the changed work item
  3. the current and previous values of the effort fields, which are in the eventXml

To be able to read from the eventXml, I have created a seperate class to have a seperation of concerns called EventXmlHelper. There is one method in there that reads the EventXml and has some arguments to define what you want to read

    using System;
    using System.Xml;
    public class EventXmlHelper
    {
        public enum FieldSection
        {
            CoreFields,
            ChangedFields
        }

        public enum FieldType
        {
            IntegerField,
            StringField
        }

        public enum ValueType
        {
            NewValue,
            OldValue
        }

        public static T GetWorkItemValue<T>(string eventXml, FieldSection section, FieldType type, ValueType valueType, string refName)
        {
            var path = string.Format("/WorkItemChangedEvent/{0}/{1}s/Field[ReferenceName='{3}']/{2}", section, type, valueType, refName);

            var doc = new XmlDocument();
            doc.LoadXml(eventXml);

            var node = doc.SelectSingleNode(path);

            object text;
            if (node == null)
            {
                if (typeof(T) == typeof(int))
                {
                    text = 0;
                }
                else if (typeof(T) == typeof(string))
                    {
                        text =  "";
                    }
                else
                {
                    throw new NotImplementedException();
                }
            }
            else
            {
                text = node.InnerText;
            }

            return (T)Convert.ChangeType(text, typeof(T));
        }
    }

To be able to communicate with TFS, I also created a helper class. This class opens the connection to TFS and is able to open the parent work item.

    using System;
    using System.Linq;
    using System.Xml;
    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.WorkItemTracking.Client;
    public class TfsHelper
    {
        public TfsTeamProjectCollection TfsInstance { get; set; }

        public TfsHelper(string tfsIdentityXml)
        {
            //Get the url from the tfsIdentity xml.
            var doc = new XmlDocument();
            doc.LoadXml(tfsIdentityXml);

            if (doc.FirstChild == null) throw new Exception("url not found");
            var url = doc.FirstChild.Attributes["url"].Value;

            //the url is in the form of http://localhost:8080/tfs/DefaultCollection/Services/v3.0/LocationService.asmx
            //strip the /Services/v3.0/LocationService.asmx part
            url = url.Substring(0, url.Length - ("/Services/v3.0/LocationService.asmx").Length);

            // Instantiate a reference to the TFS Project Collection
            TfsInstance = new TfsTeamProjectCollection(new Uri(url));
        }

        /// <summary>
        /// Returns the work item with the specified id
        /// </summary>
        public WorkItem OpenWorkItem(int id)
        {
            var store = (WorkItemStore)TfsInstance.GetService(typeof(WorkItemStore));
            return store.GetWorkItem(id);
        }

        /// <summary>
        /// Returns the parent work item of the work item with the specified id. When it has no parent, null is returned.
        /// </summary>
        public WorkItem OpenParentWorkItem(int id)
        {
            // Get the work item with the specified id
            var workItem = OpenWorkItem(id);

            // Get the link to the parent work item through the work item links
            var q = from l in workItem.WorkItemLinks.OfType<WorkItemLink>()
                    where l.LinkTypeEnd.LinkType.LinkTopology == WorkItemLinkType.Topology.Tree
                    && !l.LinkTypeEnd.IsForwardLink
                    select l.TargetId;

            // If there is a link with a parent work item
            if (q.Count() > 0)
            {
                // Return that one
                return OpenWorkItem(q.ElementAt(0));
            }
            else
            {
                return null;
            }

        }

    }

Update the hours of the parent work item

With these helper files, rolling up the hours is a piece of cake. However you still have to harden your code to deal with errors.

        void IRollupService.Notify(string eventXml, string tfsIdentityXml)
        {
            const string refNameEstimate = "Microsoft.VSTS.Scheduling.OriginalEstimate";
            const string refNameRemaining = "Microsoft.VSTS.Scheduling.RemainingWork";
            const string refNameCompleted = "Microsoft.VSTS.Scheduling.CompletedWork";

            // Extract the required information out of the eventXml
            var workItemId = EventXmlHelper.GetWorkItemValue<int>(eventXml, EventXmlHelper.FieldSection.CoreFields,
                                                                  EventXmlHelper.FieldType.IntegerField,
                                                                  EventXmlHelper.ValueType.NewValue, "System.Id");
            var oldOriginalEstimate = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                           EventXmlHelper.FieldSection.ChangedFields,
                                                                           EventXmlHelper.FieldType.StringField,
                                                                           EventXmlHelper.ValueType.OldValue,
                                                                           refNameEstimate);
            var newOriginalEstimate = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                           EventXmlHelper.FieldSection.ChangedFields,
                                                                           EventXmlHelper.FieldType.StringField,
                                                                           EventXmlHelper.ValueType.NewValue,
                                                                           refNameEstimate);
            var oldRemainingWork = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                        EventXmlHelper.FieldSection.ChangedFields,
                                                                        EventXmlHelper.FieldType.StringField,
                                                                        EventXmlHelper.ValueType.OldValue,
                                                                        refNameRemaining);
            var newRemainingWork = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                        EventXmlHelper.FieldSection.ChangedFields,
                                                                        EventXmlHelper.FieldType.StringField,
                                                                        EventXmlHelper.ValueType.NewValue,
                                                                        refNameRemaining);
            var oldCompletedWork = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                        EventXmlHelper.FieldSection.ChangedFields,
                                                                        EventXmlHelper.FieldType.StringField,
                                                                        EventXmlHelper.ValueType.OldValue,
                                                                        refNameCompleted);
            var newCompletedWork = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                        EventXmlHelper.FieldSection.ChangedFields,
                                                                        EventXmlHelper.FieldType.StringField,
                                                                        EventXmlHelper.ValueType.NewValue,
                                                                        refNameCompleted);

            // Create a new TFS helper
            var tfsHelper = new TfsHelper(tfsIdentityXml);

            // Get the parent work item
            var parentWorkItem = tfsHelper.OpenParentWorkItem(workItemId);

            // If there is one
            if (parentWorkItem != null)
            {
                // Update the effort fields with the difference between the old and the new value
                parentWorkItem.Fields[refNameEstimate].Value =
                    Convert.ToInt32(parentWorkItem.Fields[refNameEstimate].Value) + newOriginalEstimate -
                    oldOriginalEstimate;
                parentWorkItem.Fields[refNameRemaining].Value =
                    Convert.ToInt32(parentWorkItem.Fields[refNameRemaining].Value) + newRemainingWork - oldRemainingWork;
                parentWorkItem.Fields[refNameCompleted].Value =
                    Convert.ToInt32(parentWorkItem.Fields[refNameCompleted].Value) + newCompletedWork - oldCompletedWork;

                parentWorkItem.Save();
            }
        }

Test and debug the service

To test your service, you can create two task work items which have a parent/child relation. Now attach Visual Studio to the w3wp process that hosts the web service (Tools –> Attach to Process) and set a breakpoint in your code. Then modify the effort fields in the child work item and wait. It can take 1 to 2 minutes before the event is processed and your breakpoint is hit. See debugging tip #2 how to change this.

Tips for debugging:

#1: When the service is not working as you would expect, you can follow the post of Grant Holiday how to see the errors the event system is having http://blogs.msdn.com/b/granth/archive/2009/10/28/tfs2010-diagnosing-email-and-soap-subscription-failures.aspx

#2: The Job Agent Service, which is the agent that calls our service, is configured by default to run every 2 minutes. You can change the setting by following the instructions at http://blogs.msdn.com/b/chrisid/archive/2010/03/05/faster-delivery-of-notifications.aspx

Tags:

TFS SDK | VSTS 2010 | Work items

Time travelling with Work Item Queries In TFS 2010

by Ewald Hofman 20. April 2010 05:28

Someone asked me if I could help him because he made a mess of the work items. He published old data from Excel to TFS, so all the updates of his team mates where gone with one click.

There is a nice and very under-appreciated feature in the work item queries to get the values of the work items at a moment in history as if you created a snapshot at that time. It is pretty easy to accomplish this with the ASOF feature. The following steps show you how you can use the ASOF statement.

  1. Create a new query with the fields and filters you are interested in via the default “Add query” command
  2. Save the query (File –> Save New Query 1 [Query])
  3. You get now the ability to store the query on the file system 

    TimeTravelling002
  4. Select an appropriate location and click Save
  5. Open the file you just saved in Notepad. You will now see the Work Item Query Language (WIQL) that is used to define the query. For more information on the syntax, please refer to http://msdn.microsoft.com/en-us/library/bb130198.aspx
    This WIQL could look like:

    <?xml version="1.0" encoding="utf-8"?><WorkItemQuery Version="1"><TeamFoundationServer>http://myserver:8080/tfs/defaultcollection</TeamFoundationServer><TeamProject>Agile</TeamProject><Wiql>SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.AssignedTo] = 'Ewald Hofman' ORDER BY [System.Id]</Wiql></WorkItemQuery>
  6. You can only execute a modified WIQL with a .NET app. So lets create a new C# ConsoleApplication project
  7. Add the references to
    1. Microsoft.TeamFoundation.Client
    2. Microsoft.TeamFoundation.Common
    3. Microsoft.TeamFoundation.WorkItemTracking.Client
    4. System.Windows.Forms

      you can find the TeamFoundation assemblies in %Program Files%\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0
  8. Add the following using statements to your code

    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.WorkItemTracking.Client;
    using System.Net;
    using System.Windows.Forms;
  9. Now add the following code to the class Program. You will have to modify the tfsServer and queryText variables to your own values. Notice at the end of the queryText the ASOF syntax. With this statement you are time travelling.
    [STAThread()]
    static void Main(string[] args)
    {
    
    
        StringBuilder clipboardText = new StringBuilder();
        bool titlesPrinted = false;
    
    
        string tfsServer = @"http://myserver:8080/tfs/defaultcollection";
        string queryText = @"SELECT [System.Id], [System.Title], [System.State] FROM WorkItems 
                                WHERE [System.AssignedTo] = 'Ewald Hofman' ORDER BY [System.Id] ASOF '4/10/2010'";
    
        // Open the connection to TFS
        using (var tfs = new TfsTeamProjectCollection(new Uri(tfsServer), CredentialCache.DefaultCredentials))
        {
    
            // Get the work item service
            var store = (WorkItemStore)tfs.GetService(typeof(WorkItemStore));
    
            // Execute the query
            var wiCollection = store.Query(queryText);
    
            // Iterate through all work items
            foreach (WorkItem wi in wiCollection)
            {
                // Add the column headers
                if (!titlesPrinted)
                {
                    titlesPrinted = true;
    
                    foreach (FieldDefinition field in wiCollection.DisplayFields)
                    {
                        clipboardText.Append(field.Name);
                        clipboardText.Append("\t");
                    }
                    clipboardText.AppendLine();
                }
    
                // Add the work item values
                foreach (FieldDefinition field in wiCollection.DisplayFields)
                {
                    clipboardText.Append(wi.Fields[field.Name].Value);
                    clipboardText.Append("\t");
                }
                clipboardText.AppendLine();
            }
        }
    
        // Put the complete text to the clipboard, so it can be copied to Excel
        if (clipboardText.Length > 0)
            SetClipboard(clipboardText.ToString());
    
    
    
    }
    
    /// <summary>
    /// Try a few times to put it on the clipboard: known issue...
    /// </summary>
    public static void SetClipboard(object data)
    {
        for (int i = 0; i < 10; i++)
        {
            try
            {
                Clipboard.SetDataObject(data);
                return;
            }
            catch { }
            System.Threading.Thread.Sleep(100);
        }
    }
  10. You can now run the code, open Excel and start paste command.

Tags:

TFS SDK | VSTS 2010 | Work items

Time travelling with Work Item Queries In TFS 2008

by Ewald Hofman 20. April 2010 04:51

Someone asked me if I could help him because he made a mess of the work items. He published old data from Excel to TFS, so all the updates of his team mates where gone with one click.

There is a nice and very under-appreciated feature in the work item queries to get the values of the work items at a moment in history as if you created a snapshot at that time. It is pretty easy to accomplish this with the ASOF feature. The following steps show you how you can use the ASOF statement.

  1. Create a new query with the fields and filters you are interested in via the default “Add query” command
  2. Save the query (File –> Save New Query 1 [Query])
  3. You get now the ability to store the query on the file system

    TimeTravelling001
  4. Select an appropriate location and click Save
  5. Open the file you just saved in Notepad. You will now see the Work Item Query Language (WIQL) that is used to define the query. For more information on the syntax, please refer to http://msdn.microsoft.com/en-us/library/bb130198.aspx
    This WIQL could look like:

    <?xml version="1.0" encoding="utf-8"?><WorkItemQuery Version="1"><TeamFoundationServer>http://myserver:8080/</TeamFoundationServer><TeamProject>TestTeam</TeamProject><Wiql>SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.AssignedTo] = 'Ewald Hofman' ORDER BY [System.Id]</Wiql></WorkItemQuery>
  6. You can only execute a modified WIQL with a .NET app. So lets create a new C# ConsoleApplication project
  7. Add the references to
    1. Microsoft.TeamFoundation.Client
    2. Microsoft.TeamFoundation.Common
    3. Microsoft.TeamFoundation.WorkItemTracking.Client
    4. System.Windows.Forms

      you can find the TeamFoundation assemblies in %Program Files%\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies
  8. Add the following using statements to your code

    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.WorkItemTracking.Client;
    using System.Net;
    using System.Windows.Forms;
  9. Now add the following code to the class Program. You will have to modify the tfsServer and queryText variables to your own values. Notice at the end of the queryText the ASOF syntax. With this statement you are time travelling.
    [STAThread()]
    static void Main(string[] args)
    {
    
    
        StringBuilder clipboardText = new StringBuilder();
        bool titlesPrinted = false;
    
    
        string tfsServer = @"http://myserver:8080";
        string queryText = @"SELECT [System.Id], [System.Title], [System.State] FROM WorkItems 
    WHERE [System.AssignedTo] = 'Ewald Hofman' ORDER BY [System.Id] ASOF '4/10/2010'"
    ; // Open the connection to TFS using (var tfs = new TeamFoundationServer(tfsServer, CredentialCache.DefaultCredentials)) { // Get the work item service var store = (WorkItemStore)tfs.GetService(typeof(WorkItemStore)); // Execute the query var wiCollection = store.Query(queryText); // Iterate through all work items foreach (WorkItem wi in wiCollection) { // Add the column headers if (!titlesPrinted) { titlesPrinted = true; foreach (FieldDefinition field in wiCollection.DisplayFields) { clipboardText.Append(field.Name); clipboardText.Append("\t"); } clipboardText.AppendLine(); } // Add the work item values foreach (FieldDefinition field in wiCollection.DisplayFields) { clipboardText.Append(wi.Fields[field.Name].Value); clipboardText.Append("\t"); } clipboardText.AppendLine(); } } // Put the complete text to the clipboard, so it can be copied to Excel if (clipboardText.Length > 0) Clipboard.SetText(clipboardText.ToString(), TextDataFormat.Text); }
  10. You can now run the code, open Excel and start paste command.

Tags:

TFS SDK | VSTS 2008 | Work items

Allow in TFS 2010 Team Query contributions by non-Project Administrators

by Ewald Hofman 13. November 2009 05:34

Team Queries are a great way to share queries over the team. In TFS 2008 you had to have the role of a Project Administrator to create Team Queries. Since Project Administrators can do much more, such as the adding and removing users to the team project and change the Team Project settings, you do not want to give everybody this role. In TFS 2008 you had to find workarounds to share the query with the team, such as storing it in SharePoint, saving it as a file, or by sending over the query string.

In TFS 2010, you can now adjust this yourself. By default, only the Project Admin is able to add, modify and delete Team Queries.

To adjust this, open the context menu on the folder you want to change.

image

Select the Security option. This shows you a dialog in which you can adjust the security for this folder.

image

You can for example create a `Shared Queries´ folder. You want to allow that the Contributors can add new Team Queries. You can do that by selecting the Contributor role in the upper list, and then check the Contribute permission on the lower list.

image

Press OK, and we are one big happy family again that is able to share our valuable information.

Tags:

VSTS 2010 | Work items

Import test cases from Excel

by Ewald Hofman 6. November 2009 13:28

On of the pillars of TFS 2010 is the “No More No Repro”. This statement is based on the exceptional tooling to improve the communication and information hand-over between the developer and the tester. The tooling that is used is the IntelliTrace (aka Historical Debugging) and the Test Center.

The Test Center is heavily based on the Test Case work item. That work item contains the steps for the test case, which are shown when executing the test case.

 ScreenShot001

That is very nice, but what to do with all those test cases that we have already defined in other applications. And what to do with testers who does not want to leave their own tooling, such as Excel. For those situation, Microsoft has created a Test Case Importer tool on Codeplex. This post shows you what you can expect from that importer.

You can download the importer from: http://tcmimport.codeplex.com.

When you first start the importer you will see the following screen

ScreenShot002

The importer will guide you through a couple of steps to give enough information how and what to import. In this post the following sheet is used as example.

ScreenShot003

First it asks the destination of the test steps.

 image

Then the wizard asks for the destination. This is a combination of the TFS Server, the team project and the work item type.

image

The next step is to create a mapping between the columns in Excel and the fields in the work item

image

In the Data Mapping step you have the possibility to create a mapping between values in Excel and lists in the TFS work item, for example for the Priority field.

When you then start the import, a progress dialog appears

image

When the importer is ready, you have created for each row a new test case work item which you can use to do the further testing.

I am very satisfied that Microsoft has provided a tool to import test cases that were created in the past. Can’t wait to convert all those test cases in the excellent TFS 2010 environment and make my product very stable.

Tags:

VSTS 2010 | Work items | Test Essentials

TFS 2008 API - Create a link between work items

by Ewald Hofman 3. November 2009 11:33

With the new version of TFS on the horizon, which supports hierarchical links out of the box with a cool user interface to quickly create a nice work breakdown, many of us are still stuck to TFS 2008 (which of course has many great features :) ). This post shows you how you can create the links by making use of the TFS API to quickly simulate a breakdown. Especially when you combine this post with one of my previous posts Report on Work breakdown in TFS.

To make this example working, add the following using statements to your file

using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

First start with the following code snippet to get the Team Project that contains the work items you want to link

var server = new TeamFoundationServer("myServer");
var store = (WorkItemStore)server.GetService(typeof(WorkItemStore));
var project = store.Projects["myTeamProject"];

then you have to find the ‘parent work item’ you want to add the work item links to (in the example the work item 1234)

var parentWI = store.GetWorkItem(1234);

This post will add all work items that are returned by a team query, so first we have to find the team query

private static StoredQuery GetTeamQuery(Project project, string queryName)
{
        foreach (StoredQuery query in project.StoredQueries)
        {
                if (string.Compare(query.Name, queryName, true) == 0) return query;
        }

        return null;
}

Then of course we need to retrieve the query and make it executable by replacing all parameters in the query.

var query = GetTeamQuery(project, QueryTextbox.Text);
var querystring = query.QueryText.Replace("@project", "'" + project.Name + "'");
var wic = project.Store.Query(querystring);

Then iterate through the results of the query, and add all the work items to links of the ‘parent’ work item

foreach (WorkItem wi in wic)
{
     parentWI.Links.Add(new RelatedLink(wi.Id));
}

Finally you have to save the 'parent' work item

parentWI.Save();

You can of course use this example as base for your solution to create some sort of auto-linkin of work items.

Good luck.

Tags:

VSTS 2008 | Work items

[Update] Time sheet

by Ewald Hofman 4. October 2009 01:08

A lot of software shops want to use TFS as the time tracking tool. Out of the box, TFS does not provide this information, because all time is entered cumulative per work item. But you can find the information when you look at the transaction database table where all the changes to the work items are stored.

This is exactly what I did. I created a report to read from the transaction table to present the information per user per day. This leads to report as below.

 

The post shows you how you can find this information yourself. At the bottom of the post you can find the rdl.

The base of the report is based on the transaction table of the work items. This table is called "Work Item History". The tricky part of the query is to find for each transaction record (which is a change to - or revision of - the work item) its previous revision. When you have that information, you can calculate the difference between the two records. This difference is the amount of time the developer has added to the completed work, and thus the hours registered.

The Sql statement that gives you this information is the following.

SELECT
    ...
FROM
            dbo.[Work Item History]
    INNER JOIN    dbo.[Work Item]
                    ON    dbo.[Work Item History].[Work Item]        = dbo.[Work Item].__ID
    LEFT JOIN    (            dbo.[Work Item History] [Previous Work Item History] 
                INNER JOIN    dbo.[Work Item] [Previous Work Item]
                                ON    [Previous Work Item History].[Work Item]    = [Previous Work Item].__ID 
            )
                ON    [Previous Work Item].System_Id            = [Work Item].System_Id
                AND    [Previous Work Item].System_Rev            = [Work Item].System_Rev - 1
                AND    [Previous Work Item History].[Record Count]    = -1
 

For each transaction there is a negative record to set the values to 0 and a positive record to set the values to the actual values. For the previous revision we are only interested in the positive record, which is decorated with the "Record Count = -1.

When you have this starting point, you can add more tables to support filtering the data, such as on Team Project, Area path, Iteration path, Assignee, etc. I did add those four filters to the report. This resulted in the report that is attached to the blog post.

This report is view only. Notion solution has created a nice product that adds the ability to enter your time sheet from within Visual Studio. For more information on that solution, see http://www.notionsolutions.com/Products/Pages/NotionTimesheet.aspx

TimeSheet.rdl (29.97 kb)

Tags:

Reporting | VSTS 2008 | Work items

Free download of TFS Work Item Manager & TFS Project Dashboard

by Ewald Hofman 9. September 2009 11:26

Inspired by agile development methodologies, these two free applications have been designed to make working with Microsoft’s Team Foundation Server faster and easier. By promoting robust yet flexible project management practices, TFS Work Item Manager Beta and TFS Project Dashboard Beta allow for rapid delivery of high-quality software. Both tools have been used extensively internally at Telerik and Imaginet for over six months with great results. The applications have been built by Imaginet Resources using RadControls for WPF and are available for free download.

Read more on this interesting offer at http://www.telerik.com/products/tfsmanager-and-tfsdashboard.aspx

 

Tags:

VSTS 2010 | Work items

Renamed or deleted AD user prevents work item to save

by Ewald Hofman 31. August 2009 00:43

When a user is removed from AD, all work items the user has created and/or changed can't be saved anymore. To overcome this issue, you can do the following

Step Description
1 Download the TFS 2008 power tools from http://www.microsoft.com/downloads/details.aspx?FamilyID=00803636-1d16-4df1-8a3d-ef1ad4f4bbab&displaylang=en 
2

Create a new xml file with the following contents:

<?xml version="1.0" encoding="utf-8" ?>
<TfsWitDisplayNameChanges>
  <!-- List of field reference names to update. -->
  <Fields>
    <!-- Core "Person" Fields -->
    <Field refName="System.AssignedTo" />
    <Field refName="System.ChangedBy" />
    <Field refName="System.CreatedBy" />
    
    <!-- Non-Core "Person" Fields -->
    <Field refName="Microsoft.VSTS.Common.ActivatedBy" />
    <Field refName="Microsoft.VSTS.Common.ClosedBy" />
    <Field refName="Microsoft.VSTS.Common.ResolvedBy" />
 
    <!-- Add any custom "Person" fields here. -->
  </Fields>
  
  <!-- List of old and new values. -->
  <Values>
    <!-- Display name format changes. -->
<Value oldValue="Doe, Jane" newValue="Jane Doe" deleted="false"/>
 
    <!-- Display names for a deleted user -->
    <Value oldValue="John Q." newValue="John Q. Public" deleted="true" />
 
    <!-- Someone gets married and their name changes. -->
    <Value oldValue="Amy Smith" newValue="Amy Jones" deleted="false"/>
 
    <!-- Add additional Value elements here. -->
  </Values>
</TfsWitDisplayNameChanges>

 

3 Change the mapping file, so it reflects your own users
4 Save the mapping file
5 Open a new command prompt, you must start the command prompt with a user account that has access to the TFS databases
6 In the command prompt change the directory to %Program files%\Microsoft Team Foundation Server 2008 Power Tools
7

type the following command:

tfsusers.exe update /server:<tfsserver> /mappingfile:<location of mapping file>

Now the database has changed all entries in the work item of the old username to the new user name. It is not ideal, but you can at least save your work items again.

You might consider to not delete user accounts from AD, but to disable them to not be forced to do this action every time.

Tags:

VSTS 2008 | Work items

Useful usage of MS Project

by Ewald Hofman 17. August 2009 06:27

Problem

When I heard from TFS the first time in 2005 I was very delighted to see the MS Project integration with TFS. When starting my first project in TFS, I immediately started to use MS Project to plan the project. Finally I was able to see what the impact would be for the project and also use it to communicate very well with the developers, testers, etc.

After the first week I realized that MS Project is very nice to show an initial (high level) project plan, but to use it for progress tracking it is not the best tool. Let's tell why.

Take a very simple project plan

 

I upload the data to TFS, and I am going to work on the second task. The following day I level the resources to see what the changes are to the original project plan. The result of this is:

 

According to this plan, I have done nothing on monday and that I will have no tasks for today. This leads to a delay in my project plan of a day, although in reality there is no delay at all. I would have expected the following plan:

 

This behaviour is by design in MS Project: any completion of a task will not be moved when leveling the resources. And this behaviour did make me decide that to not use MS Project anymore.

Solution

Lately a new user wanted to use MS Project and asked me whether it was possible to work with MS Project. I discussed with him why he shouldn't use MS Project, but then he came with a brilliant idea. He wanted to see the remaining work only. This made me think.

Imagine that the Completed Work will not be synchronized with TFS, then a task would be always 0% completed, so the remaining work of the task will be moveable, and the completed part of the task will disappear from the project plan. Let's try that to our example.

Starting with the original plan

Then mark the second task as completed. This is now done by setting the remaining work of the task to 0. This make the following plan when leveling the resources at

 

This gives me exactly what I am looking for: it shows me where my critical path is and what the finish date for the project will be.

Implementation

To implement this solution, do the following steps

  1. Open the Visual Studio Command prompt (Start -> All Programs -> Microsoft Visual Studio 2008 -> Visual Studio Tools -> Visual Studio 2008 Command Prompt)

  2. Type "tfsfieldmapping download <tfsserver> <team project> <local file>, for example tfsfieldmapping download http://MyTfsServer:8080 MyTeamProject c:\temp\mapping.xml

  3. Open the downloaded file (in the example c:\temp\mapping.xml) in a xml editor (eg. in Visual Studio)

  4. Remove the line

    <

     

     

    Mapping WorkItemTrackingFieldReferenceName="Microsoft.VSTS.Scheduling.CompletedWork" ProjectField="pjTaskActualWork" ProjectUnits="pjHour" />

  5. Save the file

  6. Type "tfsfieldmapping upload <tfsserver> <team project> <local file>, for example tfsfieldmapping upload http://MyTfsServer:8080 MyTeamProject c:\temp\mapping.xml

  7. When you create a new MS Project and connect it to the TFS server, there is no synchronization of the completed work, so when you refresh the work items, only the remaining work will be updated and the completed work remains 0.

Tags:

VSTS 2008 | Work items

Make the description field formattable

by Ewald Hofman 4. August 2009 05:41

By default the description field in the work item definition (for example the Task work item), is plain text. To modify the type of the field to Html, you have to do the following:

Step Action
1 Open the command prompt (Run as Administrator)
2 Go to the folder %Program Files%\Microsoft Visual Studio 10.0\Common7\IDE
3

Export the work item with the following command:

witadmin exportwitd /s http:// <server> :<port>/<virtual directory>/<project collection> /p <team project> /n <work item type name> /f <location>
Example:
witadmin exportwitd /s http://MyTfsServer:8080/tfs/DefaultCollection /p TestProject /n Task /f c:\task.xml
4 Open the work item definition (in the example c:\task.xml) in Visual Studio or any other xml editor
5

Go to the FIELD

      <FIELD name="Description" refname="System.Description" type="PlainText">
        <HELPTEXT>What to do, pointers to resources and inputs, design notes, exit criteria</HELPTEXT>
      </FIELD>

6

You cannot change the type of an existing field, so just add a new field. Add a new FIELD section to the xml file:

      <FIELD name="DescriptionHtml" refname="Custom.DescriptionHtml" type="HTML">
        <HELPTEXT>What to do, pointers to resources and inputs, design notes, exit criteria</HELPTEXT>
      </FIELD>

7

Now change the field that is shown in the description textbox. Go to the section

<Control FieldName="System.Description" Type="HtmlFieldControl" Label="&amp;Description:" LabelPosition="Top" Dock="Fill" />

8

And change it to:

<Control FieldName="Custom.DescriptionHtml" Type="HtmlFieldControl" Label="&amp;Description:" LabelPosition="Top" Dock="Fill" />

9 Save the file
10

Import the new work item definintion with the command:

witadmin importwitd /s http:// <server> :<port>/<virtual directory>/<project collection> /p <team project> /f <location>
 
Example:
 
witadmin importwitd /s http://MyTfsServer:8080/tfs/DefaultCollection /p TestProject /f c:\task.xml

When you have completed the tasks, your new Task work item will look like:

 

Tags:

VSTS 2008 | VSTS 2010 | Work items

Add roles to the assigned To field

by Ewald Hofman 4. August 2009 01:28

When you create a new work item, you don't always know who exactly should pick up the work. In that case you want to introduce a role to the work item's assigned to field. To do this, you have to make a modification to the work item:

Step Action
1 Open the command prompt (Run as Administrator)
2 Go to the folder %Program Files%\Microsoft Visual Studio 10.0\Common7\IDE
3

Export the work item with the following command:

witadmin exportwitd /s http:// <server> :<port>/<virtual directory>/<project collection> /p <team project> /n <work item type name> /f <location>
Example:
witadmin exportwitd /s http://MyTfsServer:8080/tfs/DefaultCollection /p TestProject /n Task /f c:\task.xml
 
4 Open the work item definition (in the example c:\task.xml) in Visual Studio or any other xml editor
5

Go to the line

<FIELD name="Assigned To" refname="System.AssignedTo" type="String" syncnamechanges="true" reportable="dimension">
6

Change the line <VALIDUSER/> to

 <ALLOWEDVALUES expanditems="true">
 <LISTITEM value = "[Project]\Project Administrators" />
 <
LISTITEM value = "[Project]\Contributors" />
 <
LISTITEM value = "Role1" />
 
<LISTITEM value = "Role2" />
</ALLOWEDVALUES>

- The VALIDUSER shows all users that have access to TFS in the assigned to field.

- The expanditems attribute ensures that the assigned to field shows all users in the Project Administrators and the Contributors group. When you set the expanditems to false, then the users will not be shown, but only the 4 above groups/roles

- The [Project]\... lines adds the users in those groups to the assigned to field

- The Role1 and Role2 are new roles that are available

7 Save the file
8

Import the new work item definintion with the command:

witadmin importwitd /s http:// <server> :<port>/<virtual directory>/<project collection> /p <team project> /f <location>
 
Example:
 
witadmin importwitd /s http://MyTfsServer:8080/tfs/DefaultCollection /p TestProject /f c:\task.xml

Tags:

VSTS 2010 | Work items

Automatically merge work item type modifications into existing team projects – part 3

by Ewald Hofman 21. July 2009 04:42

When you are working in an enterprise that has a lot of team projects, it is hard to make generic modifications to the work item types. Imagine that you want to add a field to all work item types in all team projects, first in the staging environment, then in the production environment. This means a lot of modifications!

Fortunately it is possible to automate this process. A work item type is nothing more then xml, what you can modify. This post shows you how you can automate this process. The post is divided into multiple parts:

  • Part 1: Download and upload the work item type from/to the TFS server
  • Part 2: Make the modifications to the work item type
  • Part 3: Add new elements to the work item type

In the first part, the base library is created to be able to download and upload the work item types from/to the TFS server. The second part added the ability to modify the attribute values of existing work item type elements. This part shows how you can enable adding new elements to work item types.

  1. Add a new class to the MergeWitModifications project and rename it to XPathAddition.cs.
  2. Add the following code to the class
  3. namespace MergeWitModifications
    {
        public class XPathAddition
        {
            /// <summary>
            /// The class that holds the name/value pair information for an attribute
            /// that is added to the node that will be added to the work item type.
            /// </summary>
            public struct Attribute
            {
                public string Name;
                public string Value;
            }
     
            /// <summary>
            /// The xPath of the parent where the node will be placed under.
            /// </summary>
            public string XPath { get; set; }
     
            /// <summary>
            /// The name of the new node.
            /// </summary>
            public string Name { get; set; }
     
            /// <summary>
            /// The list of name/value pairs that will be added to the new node.
            /// </summary>
            public Attribute[] Attributes { get; set; }
     
            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="xPath">The xPath of the parent where the node will be placed under.</param>
            /// <param name="name">The name of the new node.</param>
            /// <param name="attributes">The list of name/value pairs that will be added to the new node.</param>
            public XPathAddition(string xPath, string name, Attribute[] attributes)
            {
                XPath = xPath;
                Name = name;
                Attributes = attributes;
            }
        }
    }
  4. Navigate to the Modifications class and replace the contents of the class to
  5. using System.Collections.Generic;
    namespace MergeWitModifications
    {
        public class Modifications
        {
            public Modifications()
            {
                XPathModifications = new List<XPathModification>();
                XPathAdditions = new List<XPathAddition>();
            }
     
            /// <summary>
            /// The list contains all modifications to the attribute values
            /// that must be done in the work item type.
            /// </summary>
            public List<XPathModification> XPathModifications { get; private set; }
     
            /// <summary>
            /// The list contains all additions to the work item type.
            /// </summary>
            public List<XPathAddition> XPathAdditions { get; private set; }
        }
    }
  6. To add the logic that adds the new elements to the work item type definition, add the following method to the AutoModifyWit class
  7. private static void ExecuteAddition(XmlDocument doc, XPathAddition addition)
    {
        // Get the parent node 
        var node = doc.SelectSingleNode(addition.XPath);
     
        // If the parent is existent, and the node does not exist already
        if (node != null && node.SelectSingleNode(string.Format("{0}[@{1}='{2}']", addition.Name, addition.Attributes[0].Name, addition.Attributes[0].Value)) == null)
        {
            // Create the node
            var childNode = doc.CreateElement(addition.Name);
     
            // And set the attribute values
            foreach (var attribute in addition.Attributes)
            {
                var xmlAttribute = doc.CreateAttribute(attribute.Name);
                xmlAttribute.Value = attribute.Value;
                childNode.Attributes.Append(xmlAttribute);
            }
     
            // Add the created node to the parent
            node.AppendChild(childNode);
        }
    }
  8. And add the code to call the ExecuteAddition method to the Start method. To do this, navigate now to the lines
  9. ///////////////////////////////////
    /// 
    /// TODO: Make the modifications to the work item type
    /// handled in part 2.
    /// 
    ///////////////////////////////////
  10. And add the following code

  11. foreach (var addition in modifications.XPathAdditions)
    {
        ExecuteAddition(doc, addition);
    }
  12. Now update the unit test to test the possibility to add new elements to the work item types. Navigate to the StartAutoModifyWit unit test.
  13. Add the following line to the unit test to add the start date to all work item types on the server.
  14.             modifications.XPathAdditions.Add(new XPathAddition("//Group[@Label='Schedule']/Column", "Control",
                                                             new[]
                                                                           {
                                                                               new XPathAddition.Attribute() {Name = "FieldName", Value="Microsoft.VSTS.Scheduling.StartDate"}, 
                                                                               new XPathAddition.Attribute() {Name = "Type", Value="FieldControl"},
                                                                               new XPathAddition.Attribute() {Name = "Label", Value="Start Dat&e:"},
                                                                               new XPathAddition.Attribute() {Name = "LabelPosition", Value="Left"},
                                                                               new XPathAddition.Attribute() {Name = "ReadOnly", Value="False"}
                                                                           }));

The library is not able to modify existing elements and add new ones to the work item types. You have now the base library which you can extend to your own needs.

Good luck.

Tags:

VSTS 2008 | Work items

Automatically merge work item type modifications into existing team projects – part 2

by Ewald Hofman 21. July 2009 03:38

When you are working in an enterprise that has a lot of team projects, it is hard to make generic modifications to the work item types. Imagine that you want to add a field to all work item types in all team projects, first in the staging environment, then in the production environment. This means a lot of modifications!

Fortunately it is possible to automate this process. A work item type is nothing more then xml, what you can modify. This post shows you how you can automate this process. The post is divided into multiple parts:

  • Part 1: Download and upload the work item type from/to the TFS server
  • Part 2: Make the modifications to the work item type
  • Part 3: Add new elements to the work item type

In the first part, the base library is created to be able to download and upload the work item types from/to the TFS server. This part continues on that base library and covers modifying the work item types based on XPath expressions.

The work item type definition consists of three parts. The first part is the the declaration of the fields that are available in the work item. The second part is the layout of the work item when it is presented in Visual Studio, or Team System Web Access. The last part is the work flow for the work item. The definition of the work item type is xml based.

Modifying work item types can be done by means of changing the attributes of existent nodes, by adding new nodes, or by removing nodes. The last (removing nodes) is not covered in this series. This part of the post will handle changing the attribute values.

  1. Start by adding a new class to the MergeWitModifications project that is created in the first part. Rename the class to XPathModification.
  2. Paste the following code into the XPathModification.cs file.
  3. namespace MergeWitModifications
    {
        public  class XPathModification
        {
            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="xPath">The XPath expression to the attribute that should be modified.</param>
            /// <param name="newValue">The new value for the attribute.</param>
            public XPathModification(string xPath, string newValue)
            {
                XPath = xPath;
                NewValue = newValue;
            }
     
            public string XPath { get; private set; }
            public string NewValue { get; private set; }
     }
    }
  4. You have now created a class that holds the information to be able to change an attribute. Now add a new class that holds the list of all modifications.
  5. Add a new class to the project and call it Modifications.cs
  6. Paste the following code into the Modifications.cs file.
  7. using System.Collections.Generic;
    namespace MergeWitModifications
    {
        public  class Modifications
        {
            public Modifications()
            {
                XPathModifications = new List<XPathModification>();
            }
     
            /// <summary>
            /// The list contains all modifications to the attribute values
            /// that must be done in the work item type.
            /// </summary>
            public List<XPathModification> XPathModifications { get; private set; }
        }
    }
  8. Now it is time to create the logic to modify the work item type.
  9. Navigate to the Start method in the AutoModifyWit class and add the modifcations parameter to the method.
  10. Add a new method to the AutoModifyWit class called
  11.         /// <summary>
            /// Executes the modifications to the work items.
            /// </summary>
            /// <param name="modifications">The list of modifications that must
            /// be applied to the work item type.</param>
            /// <param name="teamProjects">The list of team projects that must be 
            /// modified. Pass null or the empty array when you want to update all 
            /// team projects.</param>
            /// <param name="workItemTypes">The list of work item types that must be 
            /// modified. Pass null or the empty array when you want to update all 
            /// work item types.</param>
            public void Start(Modifications modifications, string[] teamProjects, string[] workItemTypes)
  12. Add a new method to the class that will actually change the attribute value. To do this, navigate to the end of the AutoModifyWit class and add the following method
  13.         /// <summary>
            /// Performs the modification to the work item type definition.
            /// </summary>
            /// <param name="doc">The xml document that represents the work item type.</param>
            /// <param name="modification">The modification info.</param>
            private static void ExecuteModification(XmlDocument doc, XPathModification modification)
            {
                // Get the attribute at the XPath location
                var node = doc.SelectSingleNode(modification.XPath);
     
                if (node != null)
                {
                    // When found, modify the value of the attribute
                    node.Value = modification.NewValue;
                }
            }
  14. Navigate now to the lines
  15. ///////////////////////////////////
    /// 
    /// TODO: Make the modifications to the work item type
    /// handled in part 2.
    /// 
    ///////////////////////////////////
  16. And add the following code

  17. foreach (var modification in modifications.XPathModifications)
    {
        ExecuteModification(doc, modification);
    }
  18. Now that you have finalized the library to be able to modify a work item type, the unit test must be modified to test the code
  19. Navigate to the StartAutoModifyWit unit test that contains our "UI code".
  20. Change the call to the target.Start method to
  21. var modifications = new Modifications();
     
    target.Start(modifications, teamProjects, workItemTypes);
  22. To make the Start and Finish date writable for example you can add the following lines
  23. modifications.XPathModifications.Add(new XPathModification("//Control[@FieldName='Microsoft.VSTS.Scheduling.StartDate']/@ReadOnly", "False"));
    modifications.XPathModifications.Add(new XPathModification("//Control[@FieldName='Microsoft.VSTS.Scheduling.FinishDate']/@ReadOnly", "False"));

You have now added the functionality to modify existing elements in the work item type. The next post covers the ability to add new elements (field, layout and/or work flow) to the work item type.

Tags:

VSTS 2008 | Work items

Automatically merge work item type modifications into existing team projects – part 1

by Ewald Hofman 21. July 2009 03:09

Introduction

When you are working in an enterprise that has a lot of team projects, it is hard to make generic modifications to the work item types. Imagine that you want to add a field to all work item types in all team projects, first in the staging environment, then in the production environment. This means a lot of modifications!

Fortunately it is possible to automate this process. A work item type is nothing more then xml, what you can modify. This post shows you how you can automate this process. The post is divided into multiple parts:

  • Part 1: Download and upload the work item type from/to the TFS server
  • Part 2: Make the modifications to the work item type
  • Part 3: Add new elements to the work item type

This part of the post covers downloading and uploading the work item type from/to the TFS Server. The prerequisite is that you have installed Team Explorer onto the system that runs the application.

The application that is build, is making use of a library that wraps the functionality to communicate with the TFS server and perform the modifications to the work item type. There is a UI, which is in this case a unit test project, to tell the library what you want to change.

Preparations

  1. Open Visual Studio 2008 and create a new C# class library and call it MergeWitModifications.
  2. Add the following references to the project. The files can be found in the folder: %ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies.
    - Microsoft.TeamFoundation.dll
    - Microsoft.TeamFoundation.Client.dll
    - Microsoft.TeamFoundation.Common.dll
    - Microsoft.TeamFoundation.WorkItemTracking.Client.dll
  3. Rename the Class1.cs file to AutoModifyWit.cs
  4. Paste the following code to the file:
  5. using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Diagnostics;
    using Microsoft.TeamFoundation.Client;
    using System.Net;
    using Microsoft.TeamFoundation.WorkItemTracking.Client;
    using System.Xml;
     
    namespace MergeWitModifications
    {
        struct Info
        {
            public string Project { get; set; }
            public string WorkItemType { get; set; }
        }
        
        public class AutoModifyWit
        {
            /// <summary>
            /// Creates a new instance of this class.
            /// </summary>
            /// <param name="tfsServer">The uri to the tfs server, in the form 
            /// of http://MyTfsServer:8080.</param>
            public AutoModifyWit(string tfsServer)
            {
                TfsServer = tfsServer;
            }
     
            /// <summary>
            /// The uri to the TFS server that contains the work item types that
            /// will be modified
            /// </summary>
            private string TfsServer { get; set; }
     
            /// <summary>
            /// Executes the modifications to the work items.
            /// </summary>
            /// <param name="teamProjects">The list of team projects that must be 
            /// modified. Pass null or the empty array when you want to update all 
            /// team projects.</param>
            /// <param name="workItemTypes">The list of work item types that must be 
            /// modified. Pass null or the empty array when you want to update all 
            /// work item types.</param>
            public void Start(string[] teamProjects, string[] workItemTypes)
            {
                // The base folder where the backup of the work item type definition 
                // will be stored.
                string backupDirectory = string.Format(@"{0}\Backup{1:yyyyMMddhhmm}\", Directory.GetCurrentDirectory(), DateTime.Now);
     
                // Iterate through all work item types in all team projects. When
                // you do this without creating a new tfs instance, a write
                // access violation will occur (a bug in the TFS SDK?)
                foreach (var info in GetAllWITs())
                {
                    // Check whether the team project and/or the work item type should be skipped
                    if ((teamProjects != null && teamProjects.Length != 0 &&
                         !new List<string>(teamProjects).Contains(info.Project)) ||
                        (workItemTypes != null && workItemTypes.Length != 0 &&
                         !new List<string>(workItemTypes).Contains(info.WorkItemType)))
                        continue;
     
                    Debug.WriteLine(info.Project + ", " + info.WorkItemType);
     
                    // Open the work item store
                    using (var tfs = new TeamFoundationServer(TfsServer,
                                                              CredentialCache.DefaultCredentials))
                    {
                        var store = (WorkItemStore) tfs.GetService(typeof (WorkItemStore));
     
                        // For the Team Project
                        var project = store.Projects[info.Project];
     
                        // For the Work Item Type
                        var workItemType = project.WorkItemTypes[info.WorkItemType];
     
                        // Download the work item type from the server
                        var doc = workItemType.Export(false);
     
                        // Backup the original work item type to support a fallback scenario
                        Directory.CreateDirectory(backupDirectory + @"\" + info.Project);
                        doc.Save(backupDirectory + @"\" + info.Project + @"\" + info.WorkItemType + @".xml");
     
                        ///////////////////////////////////
                        /// 
                        /// TODO: Make the modifications to the work item type
                        /// handled in part 2.
                        /// 
                        ///////////////////////////////////
     
                        // Validate the modifications to the work item type
                        WorkItemType.Validate(project, doc.OuterXml);
     
                        // Upload the work item type to TFS
                        project.WorkItemTypes.Import(doc.OuterXml);
                    }
                }
            }
     
            /// <summary>
            /// Returns the list of all available work item types on the server
            /// </summary>
            private List<Info> GetAllWITs()
            {
                var list = new List<Info>();
     
                // Open the work item store
                using (var tfs = new TeamFoundationServer(TfsServer,
                                                          CredentialCache.DefaultCredentials))
                {
                    var store = (WorkItemStore) tfs.GetService(typeof (WorkItemStore));
                    // For all Team Projects
                    foreach (Project project in store.Projects)
                    {
                        // For all Work Item Types
                        foreach (WorkItemType workItemType in project.WorkItemTypes)
                        {
                            // Add the work item type
                            list.Add(new Info
                                         {
                                             Project = project.Name,
                                             WorkItemType = workItemType.Name
                                         });
                        }
                    }
                }
     
                return list;
            }
        }
    }
  6. Right click on the line “public void Start(string[] teamProjects, string[] workItemTypes)” and choose for the option Create Unit Tests…
  7. A dialog pops up after a while. In there you can choose the settings for the new test project. Accept the defaults by clicking on the OK button. You can name the project MergeWitModificationsTest.
  8. Paste the following code in the AutoModifyWitTest.cs file.
  9. using MergeWitModifications;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    namespace MergeWitModificationsTest
    {
        
        
        /// <summary>
        ///This is a test class for AutoModifyWitTest and is intended
        ///to contain all AutoModifyWitTest Unit Tests
        ///</summary>
        [TestClass()]
        public class AutoModifyWitTest
        {
            /// <summary>
            /// Starts the modifications of the work item types.
            ///</summary>
            [TestMethod()]
            public void StartAutoModifyWit()
            {
                // The server that contains the work item types that must be modified
                const string tfsServer = "http://MyTfsServer:8080";
     
                // The list of team projects that must be modified. Pass null or the 
                // empty array when you want to update all team projects.
                var teamProjects = new string[] { };
     
                // The list of work item types that must be modified. Pass null or the 
                // empty array when you want to update all work item types.
                var workItemTypes = new string[] { };
     
                var target = new AutoModifyWit(tfsServer);
     
                target.Start(teamProjects, workItemTypes)
            }
        }
    }

You have now created the base of the library. The library is not able to communicate with the TFS server to download and upload work item types. In the next part the library will be extended to modify the work item type with XPath expressions.

Tags:

VSTS 2008 | Work items

Report on Work breakdown in TFS

by Ewald Hofman 13. May 2009 15:32

Introduction

In Team Foundation Server 2005 and 2008 it is possible to define links between work items. By doing this, you can imply a work breakdown for your project. This work breakdown is not explicit, since these links are not hierarchical. Therefore is the only report in TFS that does something with the links not very usable. It shows all work items that have some relations.

When work item A is related to work item B, the report shows you two lines:

  1. A is related to B
  2. B is related to A

When you make the agreement in a project that:

  • The highest level of the work breakdown is always a scenario in the Agile process template, and Change Request or Requirement in the CMMI process template.
  • The second level are the tasks and the bugs that are related to the root level,
  • The third level are the bugs that are related to a task

You can now create a report that shows you the implied hierarchy of your work breakdown.

When we take it even to a further level, it is also possible to create a rollup of hours. On the second and the third level, the hours are retrieved from the warehouse, and the hours at root level are a summary of the all related tasks and bugs. When you do this, you get insight in the progression of the project.

Implementation

In TFS, there is the operational database, which is unsupported to use. The data from the operational database is aggregated in the TfsWarehouse database. The last data store of TFS is the SSAS cube which is used to create high performance calculations on your data.

For this report, we need the TfsWarehouse database. To get the three levels of the work breakdown we have to add the Work Item table three times to the query and link the tables together via the Related Work Item table. Then add some of the other tables to get all the other information you need. This might result for the CMMI process template in a query as below. To create the report for the Agile process template, you have to modify some parts like the Estimation field. I have attached the rdl file, which you can use as base for your own report.

SELECT 
    [Team Project]    = tp.[Team Project]
,    [Iteration Path]    = Iteration.[Iteration Path]
,    [Area Path]    = Area.[Area Path]
,    L0_WIType    = SUBSTRING(level0.System_WorkItemType, 1, 1)
,    L0_State        = level0.System_State
,    L0_Title        = level0.System_Title
,    L0_Estimate    = ISNULL(cw_level0._Microsoft_VSTS_CMMI_Estimate, 0)
,    L0_Completed    = ISNULL(cw_level0._Microsoft_VSTS_Scheduling_CompletedWork, 0)
,    L0_ETC        = CASE WHEN level0.System_State = 'Proposed' THEN ISNULL(cw_level0._Microsoft_VSTS_CMMI_Estimate, 0) ELSE ISNULL(cw_level0._Microsoft_VSTS_Scheduling_RemainingWork, 0) END
,    L0_EAC        = CASE WHEN level0.System_State = 'Proposed' THEN ISNULL(cw_level0._Microsoft_VSTS_CMMI_Estimate, 0) ELSE ISNULL(cw_level0._Microsoft_VSTS_Scheduling_RemainingWork, 0) + ISNULL(cw_level0._Microsoft_VSTS_Scheduling_CompletedWork, 0) END
,    L0_FinishDate    = cw_level0.Osellus_Iris_Activity_EndDate
,    L0_AssignedTo    = level0_assignedto.Person
,    L0_ResolvedBy    = level0.Microsoft_VSTS_Common_ResolvedBy
,    L0_ResolvedDate    = cw_level0.Microsoft_VSTS_Common_ResolvedDate
,    L0_ClosedBy    = level0.Microsoft_VSTS_Common_ClosedBy
,    L0_ClosedDate    = cw_level0.Microsoft_VSTS_Common_ClosedDate
,    L0_Reason    = level0.System_Reason
,    L0_WI        = level0.System_Id
,    L1_WIType    = SUBSTRING(level1.System_WorkItemType, 1, 1)
,    L1_State        = level1.System_State
,    L1_Title        = level1.System_Title
,    L1_Estimate    = ISNULL(cw_level1._Microsoft_VSTS_CMMI_Estimate, 0)
,    L1_Completed    = ISNULL(cw_level1._Microsoft_VSTS_Scheduling_CompletedWork, 0)
,    L1_ETC        = CASE WHEN level1.System_State = 'Proposed' THEN ISNULL(cw_level1._Microsoft_VSTS_CMMI_Estimate, 0) ELSE ISNULL(cw_level1._Microsoft_VSTS_Scheduling_RemainingWork, 0) END
,    L1_EAC        = CASE WHEN level1.System_State = 'Proposed' THEN ISNULL(cw_level1._Microsoft_VSTS_CMMI_Estimate, 0) ELSE ISNULL(cw_level1._Microsoft_VSTS_Scheduling_RemainingWork, 0) + ISNULL(cw_level1._Microsoft_VSTS_Scheduling_CompletedWork, 0) END
,    L1_FinishDate    = cw_level1.Osellus_Iris_Activity_EndDate
,    L1_AssignedTo    = level1_assignedto.Person
,    L1_ResolvedBy    = level1.Microsoft_VSTS_Common_ResolvedBy
,    L1_ResolvedDate    = cw_level1.Microsoft_VSTS_Common_ResolvedDate
,    L1_ClosedBy    = level1.Microsoft_VSTS_Common_ClosedBy
,    L1_ClosedDate    = cw_level1.Microsoft_VSTS_Common_ClosedDate
,    L1_Reason    = level1.System_Reason
,    L1_WI        = level1.System_Id
,    L1_FirstL2_WI    = (SELECT Min(w.System_Id) FROM [Related Current Work Items] r INNER JOIN [Current Work Item] c ON c.__TrackingId = r.[Current Work Item_TrackingIdLeft] INNER JOIN [Work Item] w ON w.__ID = c.[Work Item] AND w.System_WorkItemType IN (@Level2WIT) WHERE r.[Current Work Item_TrackingIdRight] = cw_level1.__TrackingId)
,    L2_WIType    = SUBSTRING(level2.System_WorkItemType, 1, 1)
,    L2_State        = level2.System_State
,    L2_Title        = level2.System_Title
,    L2_Estimate    = ISNULL(cw_level2._Microsoft_VSTS_CMMI_Estimate, 0)
,    L2_Completed    = ISNULL(cw_level2._Microsoft_VSTS_Scheduling_CompletedWork, 0)
,    L2_ETC        = CASE WHEN level2.System_State = 'Proposed' THEN ISNULL(cw_level2._Microsoft_VSTS_CMMI_Estimate, 0) ELSE ISNULL(cw_level2._Microsoft_VSTS_Scheduling_RemainingWork, 0) END
,    L2_EAC        = CASE WHEN level2.System_State = 'Proposed' THEN ISNULL(cw_level2._Microsoft_VSTS_CMMI_Estimate, 0) ELSE ISNULL(cw_level2._Microsoft_VSTS_Scheduling_RemainingWork, 0) + ISNULL(cw_level2._Microsoft_VSTS_Scheduling_CompletedWork, 0) END
,    L2_FinishDate    = cw_level2.Osellus_Iris_Activity_EndDate
,    L2_AssignedTo    = level2_assignedto.Person
,    L2_ResolvedBy    = level2.Microsoft_VSTS_Common_ResolvedBy
,    L2_ResolvedDate    = cw_level2.Microsoft_VSTS_Common_ResolvedDate
,    L2_ClosedBy    = level2.Microsoft_VSTS_Common_ClosedBy
,    L2_ClosedDate    = cw_level2.Microsoft_VSTS_Common_ClosedDate
,    L2_Reason    = level2.System_Reason
,    L2_WI        = level2.System_Id
FROM
            [Team Project]            tp
    INNER JOIN    [Current Work Item]        cw_level0
                ON    cw_level0.[Team Project]        = tp.__ID
    INNER JOIN    [Work Item]            level0
                ON    level0.__ID            = cw_level0.[Work Item]
                AND    level0.System_WorkItemType    IN (@RootWIT)
    INNER JOIN    [Person]                level0_assignedto
                ON    level0_assignedto.__ID        = cw_level0.[Assigned To]
    INNER JOIN    Iteration
                ON    Iteration.__ID            = cw_level0.Iteration
    INNER JOIN    Area
                ON    Area.__ID            = cw_level0.Area
    LEFT JOIN    (        [Related Current Work Items]        rel_level0_level1
            INNER JOIN    [Current Work Item]        cw_level1
                        ON    cw_level1.__TrackingId        = rel_level0_level1.[Current Work Item_TrackingIdLeft]
            INNER JOIN    [Work Item]            level1
                        ON    level1.__ID            = cw_level1.[Work Item]
                        AND    level1.System_WorkItemType     IN (@Level1WIT)
            LEFT JOIN    [Person]                level1_assignedto
                        ON    level1_assignedto.__ID        = cw_level1.[Assigned To]
            )
                ON    cw_level0.__TrackingId        = rel_level0_level1.[Current Work Item_TrackingIdRight]
    LEFT JOIN    (        [Related Current Work Items]        rel_level1_level2
            INNER JOIN    [Current Work Item]        cw_level2
                        ON    cw_level2.__TrackingId        = rel_level1_level2.[Current Work Item_TrackingIdLeft]
            INNER JOIN    [Work Item]            level2
                        ON    level2.__ID            = cw_level2.[Work Item]
                        AND    level2.System_WorkItemType    IN (@Level2WIT)
            LEFT JOIN    [Person]                level2_assignedto
                        ON    level2_assignedto.__ID        = cw_level2.[Assigned To]
            )
                ON    cw_level1.__TrackingId        = rel_level1_level2.[Current Work Item_TrackingIdRight]
WHERE
    tp.[Team Project]        = @Project
AND    Area.[Area Path]        LIKE @AreaParam + '%'
AND    Iteration.[Iteration Path]    IN (@IterationParam)

WorkBreakDown.rdl (164,69 kb)

Tags:

Reporting | Work items

Work item query as input for a RS report

by Ewald Hofman 11. May 2009 15:17

When you want to create a report that contains detailed data, not all fields are in the TFS warehouse. Think of text fields like description, how to reproduce, and so on. To be able to add these fields to your report, you have to write a web service that reads from a Work Item Query. This web service can then be used as data source for your report.

To create this web service do the following:

  1. Open Visual Studio
  2. Create a new project of the type “ASP.NET web service application”, name the project ReportingService
  3. Rename the Service1.asmx to WIQuery.asmx
  4. Paste the following code to the WIQuery.asmx.cs file
  5. using System;
    using System.Collections;
    using System.ComponentModel;
    using System.Data;
    using System.IO;
    using System.Web.Services;
    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.WorkItemTracking.Client;
    using System.Text.RegularExpressions;
    using System.Xml;
     
     
    namespace ReportingService
    {
        [WebService(Namespace = "http://www.avanade.com/")]
        [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
        [ToolboxItem(false)]
        // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
        // [System.Web.Script.Services.ScriptService]
        public class WIQuery : System.Web.Services.WebService
        {
     
            /// <summary>
            /// Opens the stored query.
            /// </summary>
            /// <param name="tfsUri">The uri to the TFS Server (eg. http://MyTfsServer:8080)</param>
            /// <param name="teamProject">The team project where the query is located.</param>
            /// <param name="queryName">Name of the query that should be executed (case sensitive!).</param>
            /// <returns></returns>
            [WebMethod(Description = "Use the web service to execute a TFS Work Item Query. Use the following syntax as Query Text in the report (the query name is case sensitive!):<br>" +
                @"&ltQuery&gt" + "<br>" +
                @"&ltSoapAction&gthttp://www.avanade.com/OpenStoredQuery&lt/SoapAction&gt" + "<br>" +
                @"&ltMethod Namespace=""http://www.avanade.com/""  Name=""OpenStoredQuery""&gt" + "<br>" +
                @"&ltParameters&gt" + "<br>" +
                @"&ltParameter Name=""tfsUri""&gt" + "<br>" +
                @"&ltDefaultValue&gtMyTfsUri (eg. http://tfsserver:8080)&lt/DefaultValue&gt" + "<br>" +
                @"&lt/Parameter&gt" + "<br>" +
                @"&ltParameter Name=""teamProject""&gt" + "<br>" +
                @"&ltDefaultValue&gtMyTeamProject&lt/DefaultValue&gt" + "<br>" +
                @"&lt/Parameter&gt" + "<br>" +
                @"&ltParameter Name=""queryName""&gt" + "<br>" +
                @"&ltDefaultValue&gtMyQuery&lt/DefaultValue&gt" + "<br>" +
                @"&lt/Parameter&gt" + "<br>" +
                @"&lt/Parameters&gt" + "<br>" +
                @"&lt/Method&gt" + "<br>" +
                @"&lt/Query&gt")]
            public XmlDocument OpenStoredQuery(string tfsUri, string teamProject, string queryName)
            {
                // Execute the query
                WorkItemCollection workItems = ExecuteQuery(tfsUri, teamProject, queryName);
     
                // Create a new datatable based on the display fields stored in the query
                DataTable dt = CreateDatatable(workItems);
     
                // Populate the data table with the results of the query
                foreach (WorkItem wi in workItems)
                    AddDataRow(dt, wi);
     
                // Convert the datatable to xml, because when returning the datatable, the 
                // report will show the schema
                return ConvertToXml(dt);
            }
     
            /// <summary>
            /// Converts a datatable to XML.
            /// </summary>
            /// <param name="dt">The datatable that will be converted.</param>
            private XmlDocument ConvertToXml(DataTable dt)
            {
                using (MemoryStream stream = new MemoryStream())
                {
                    // Remove the schema from the datatable
                    dt.WriteXml(stream, XmlWriteMode.IgnoreSchema);
                    var xmlDoc = new XmlDocument();
                    stream.Position = 0;
                    xmlDoc.Load(stream);
     
                    // Return the xml docuemnt that contains only the 
                    // contents of the datatable
                    return xmlDoc;
                }
            }
     
            /// <summary>
            /// Creates the datatable.
            /// </summary>
            /// <param name="coll">The workitem collection that contains the results of the query.</param>
            private DataTable CreateDatatable(WorkItemCollection coll)
            {
                 // Create a new table
                DataTable dt = new DataTable();
                dt.TableName = "wi";
     
                // Add a new column for each visible field
                foreach (FieldDefinition field in coll.DisplayFields)
                {
                    // All fields must have for its DefaultValue the value string.Empty to ensure
                    // that with the conversion to XML by WriteXML the empty fields are written as
                    // empty nodes . It is not possible to set String.Empty for numeric fields, so
                    // that fields are typed as object.
                    var column = new DataColumn
                                            {
                                                AllowDBNull = true,
                                                ColumnName = field.Name,
                                                DataType = typeof(String),
                                                DefaultValue = string.Empty
                                            };
     
                    dt.Columns.Add(column);
                }
     
                // Return the table
                return dt;
            }
     
            /// <summary>
            /// Adds a data row to the datatable based on the contents of the work item. Only fields in the display fields of the work
            /// item query will be added.
            /// </summary>
            /// <param name="dt">The datatable to add the datarow to.</param>
            /// <param name="wi">The work item that will be added to the datarow.</param>
            private void AddDataRow(DataTable dt, WorkItem wi)
            {
                // Create a new array to store the values from the work item
                object[] values = new object[dt.Columns.Count];
     
                // Add for the value for each column to the array
                foreach (DataColumn column in dt.Columns)
                {
                    switch (column.ColumnName)
                    {
                        case "Task Type":
                            values[column.Ordinal] = wi.Type;
                            break;
                        default:
                            if (wi.Fields.Contains(column.ColumnName))
                            {
                                values[column.Ordinal] = FormatValue(wi.Fields[column.ColumnName]);
                            }
     
                            break;
                    }
     
                }
     
                // Add values array to the datatable
                dt.LoadDataRow(values, true);
            }
     
            /// <summary>
            /// Executes the query.
            /// </summary>
            /// <param name="teamProject">The team project.</param>
            /// <param name="queryName">Name of the query.</param>
            /// <returns></returns>
            private WorkItemCollection ExecuteQuery(string tfsUri, string teamProject, string queryName)
            {
                // Open connection to TFS
                TeamFoundationServer tfsServer = new TeamFoundationServer(tfsUri);
     
                // Get the work item 'service'
                WorkItemStore workItemStore = (WorkItemStore)tfsServer.GetService(typeof(WorkItemStore));
     
                // Search the query
                StoredQuery query = FindQuery(workItemStore, teamProject, queryName);
     
                // Populate the list of variables that might be used in the query
                Hashtable context = new Hashtable();
                context.Add("project", teamProject);
     
                // Execute the query
                return workItemStore.Query(query.QueryText, context);
            }
     
            /// <summary>
            /// Finds the query.
            /// </summary>
            /// <param name="workItemStore">The work item store.</param>
            /// <param name="teamProject">The team project.</param>
            /// <param name="queryName">Name of the query.</param>
            /// <returns></returns>
            private StoredQuery FindQuery(WorkItemStore workItemStore, string teamProject, string queryName)
            {
                // For each query in the team project
                foreach (StoredQuery query in workItemStore.Projects[teamProject].StoredQueries)
                {
                    // Query found: return it
                    if (query.Name == queryName)
                        return query;
                }
     
                // Query not found: throw an exception
                throw new Exception(string.Format("Cannot find the query '{1}' in the team project '{0}'", teamProject, queryName));
            }
     
            /// <summary>
            /// Formats the value, so Reporting Services can understand the value.
            /// </summary>
            /// <param name="field">The work item Field</param>
            private object FormatValue(Field field)
            {
                if (field.FieldDefinition.FieldType == FieldType.Html)
                {
                    // When HTML, then strip the HTML tags
                    return StripHtml((string)field.Value);
                }
                else
                {
                    // Else return the value
                    return field.Value;
                }
            }
     
            /// <summary>
            /// Strip the HTML tags from the value
            /// </summary>
            /// <param name="value">The text to be stripped</param>
            private object StripHtml(string text)
            {
                // Replace the HTML entity &lt; with the '<' character
                text = text.Replace("&lt;", "<");
     
                // Replace the HTML entity &gt; with the '>' character
                text = text.Replace("&gt;", ">");
     
                // Replace the HTML entity &amp; with the '&' character
                text = text.Replace("&amp;", "&");
     
                // Replace the HTML entity &nbsp; with the ' ' character
                text = text.Replace("&nbsp;", " ");
     
                // Replace any <br> tags with a newline
                text = Regex.Replace(text, "<br.*>", Environment.NewLine);
     
                // Remove anything between <whatever> tags
                text = Regex.Replace(text, "<.+?>", "");
     
                return text.Trim();
     
            }
        }
    }
  6. Run (CTRL+F5) the solution. The webservice will now be hosted by ASP.NET Development Server
  7. Open the Business Intelligence Development Studio to create a new report
  8. Create a new datasource and call it WIQuery.
  9. Set the type of the datasource to XML and change the ConnectionString to the url of the webservice
  10. Create a new report and use the WIQuery datasource
  11. Set the command text of your dataset to

    <Query>
        <SoapAction>http://www.avanade.com/OpenStoredQuery</SoapAction>
        <Method Namespace="http://www.avanade.com/" Name="OpenStoredQuery">
            <Parameters>
                <Parameter Name="tfsUri">
                    <DefaultValue>http://MyTfsServer:8080</DefaultValue>
                </Parameter>
                <Parameter Name="teamProject">
                    <DefaultValue>MyTeamProject</DefaultValue>
                </Parameter>
                <Parameter Name="queryName">
                    <DefaultValue>All Work Items</DefaultValue>
                </Parameter>
            </Parameters>
        </Method>
    </Query>
  12. Change the DefaultValue of the parameters to the correct values:
    1. Uri of the TFS server
    2. Name of the Team project
    3. Name of the work item query (CASE SENSITIVE!)
  13. You can now get refresh the datasource and start designing your report.

 

Happy reporting.

Tags:

Work items

Initial values in a work item

by Ewald Hofman 6. May 2009 04:50

With the introduction of TFS 2008 SP1 it is possible to prefill the values when you create a new work item in Team System Web Access (TSWA) and Work Item Web Access (WIWA). You can achieve this by adding the value intial values for the fields of the work item in the url.

http://Server:Port/wi.aspx?pname=Project&wit=Type&[Field1]=Value1&[Field2]=Value2

Replace the following arguments by using values specific to your project requirements.

Server Name of the server that is running Team System Web Access
Port Port number that Team System Web Access is using (default 8090)
Project Name of the team project where the work item will be created
Type Type of work item to create
Field Name of a field to assign an initial value
Value Initial value to assign to the field

You can specify initial field values in any order. The following example is a URL that creates a bug that has "Tell what did you encounter" as its title and "MyTfsServer\MyArea" designated as the Area Path:

http://MyTfsServer:8090/wi.aspx?pname=MyTeamProject&wit=Bug&[Title]=Tell what did you encounter&[Area Path]=MyTfsServer\MyArea

There is an easy way to get the url for the work item template. When you create a new work item in Team System Web Access, there is a button available "Copy Template URL" to get the url that reflects the changes you made to the work item.

 

 How to achieve it in Team Explorer?

In Team Explorer you can achieve this also by making use of the Power Tools of TFS 2008. In the Power Tools there the Work Item templates are introduced.
 
To create a work item template, open a new work item and fill the fields that should be applied into the Work Item template. Then right click on the canvas of the work item and select Capture template. The template is not availble under the Work Item Template node in the team explorer. You can now use the Work Item template.

Tags:

Work items

Powered by BlogEngine.NET 1.6.1.0
Theme by Mads Kristensen


ClusterMap

Statistics

Statistics created at 09 Sep 2009

121 posts
488 comments
323 raters
1853446 visit (1043 per day)
13 users online

Recent comments

Comment RSS