Introduction
WorkItem history plays a key role when it comes to project tracking and auditing. It allows us to see what all activities that happened on workitem, who changed it, when and what. In Visual Studio TFS UI, we can see these details under 'History->All changes' tab.
Same data, we can have using TFS API and moreover, UNLIKE Visual Studio TFS, we can plot data in tabular format which is sortable, exported in Excel and can be saved on local machine for future reference.
This article will explain how this can be done programmatically in the most simple way.
Highlights of this application.
- Brings all history changes for workitem including field changes, work item link added/deleted, hyperlinks, attachments and history comments.
- History is displayed in grid format having information of changed by, changed date, field name, old value, new value and comments.
- Using export to Excel feature work item changes can be exported to Excel and saved locally.
Having these data in tabular format in local Excel, gives full control and flexibility to use it in a way we want.
Using the Code
Let's see this step by step.
Step 1: Get Connected to TFS
Connect to TFS using TFS Uri
and have WorkItemStore
object.
private TfsTeamProjectCollection projectCollection;
private WorkItemStore workItemStore;
private WorkItemStore service;
private void ConnectToTFS()
{
try
{
projectCollection =
TfsTeamProjectCollectionFactory.GetTeamProjectCollection(
new Uri(txtTFSURL.Text));
service = projectCollection.GetService<WorkItemStore>();
if (service != null)
{
workItemStore = projectCollection.GetService<WorkItemStore>();
MessageBox.Show("TFS connected successfully");
}
}
catch (Exception Ex)
{
MessageBox.Show(Ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
Step 2: Get WorkItem
After connecting to TFS successfully, get WorkItem
object using entered workItem Id
.
workItem = workItemStore.GetWorkItem(Convert.ToInt32(txtWorkItemId.Text.Trim()));
This code will return object of WorkItem
and that is where all information of WorkItem
resides. One has to play with this object and extract the required information. Here, we are interested in properties 'Revisions
', 'Links
', 'Attachments
' and 'WorkItemLinkHistory
'.
'Revisions
' property is a collection of 'Revision
' object of workitem
having revision number from 1 to so on.... Code iteraters through each revision. 'Revision
' object has collection of Fields
that hold value of each WorkItem
field in that revision.
In the next steps, we will see how we can use these properties and get all changes that happened to workitem
.
Step 3: Iterate through Revisions and Fields
To get changes, we need to compare revisions and see which field values are changed. In this looping code, we will get value of field from the previous revision and compare with current revision. If value is changed, row will be added to grid having details of who changed it, when it was changed, what was changed.
string strOldValue, strNewValue;
for (int i = 1; i < workItem.Revisions.Count; i++)
{
foreach (Field field in workItem.Revisions[i].Fields)
{
if (field.Name != "Rev" && field.Name != "Changed By" &&
field.Name != "Revised Date" && field.Name != "Watermark" &&
field.Name != "Changed Date" &&
field.Name != "Hyperlink Count" && field.Name != "Related Link Count" &&
field.Name != "Attached File Count" &&
field.Name != "External Link Count")
{
strOldValue =
(workItem.Revisions[i - 1].Fields[field.Name].Value ?? "").ToString();
strNewValue = (field.Value ?? "").ToString();
if (strOldValue != strNewValue)
{
gvAllChanges.Rows.Add
(workItem.Revisions[i].Fields["Changed By"].Value.ToString(),
Convert.ToDateTime(workItem.Revisions[i].Fields["Changed Date"].Value),
field.Name, strOldValue, strNewValue, "");
}
}
}
}
Step 4: Get Hyperlinks,Externallinks and Attachments
To get Hyperlinks and ExternalLinks (i.e., links to TestResults
in MTM), we need to filter out WorkItem.Links
collection using 'BaseType
' property. All attachments can be retrieved from Workttem.Attachments
collection. These collections give a list of links/attachments but don't say when and who created them. For that, we need to look into each revision and compare fields 'Hyperlink Count
', 'External Link Count
' and 'Attached File Count
' respectively. If count is changed, then we have to get those many links from collection.
The below code explains this.
Hyperlinks
if (workItem.HyperLinkCount > 0)
{
foreach (Link link in workItem.Links)
{
if (link.BaseType.ToString() == "Hyperlink")
{
hyperLinkColl.Add((Hyperlink)link);
}
}
}
In revisions loop:
prevCount = Convert.ToInt32(workItem.Revisions[i - 1].Fields["Hyperlink Count"].Value);
currCount = Convert.ToInt32(workItem.Revisions[i].Fields["Hyperlink Count"].Value);
if (currCount > prevCount)
{
for (int j = prevCount; j < (hyperLinkColl.Count - (hyperLinkColl.Count - currCount)); j++)
{
gvAllChanges.Rows.Add(workItem.Revisions[i].Fields["Changed By"].Value.ToString(),
Convert.ToDateTime(workItem.Revisions[i].Fields["Changed Date"].Value),
"Hyperlink", "", hyperLinkColl[j].Location, hyperLinkColl[j].Comment);
}
}
External Links
if (workItem.ExternalLinkCount > 0)
{
foreach (Link link in workItem.Links)
{
if (link.BaseType.ToString() == "ExternalLink")
externalLinkColl.Add((ExternalLink)link);
}
}
In revisions loop:
prevCount = Convert.ToInt32(workItem.Revisions[i - 1].Fields["External Link Count"].Value);
currCount = Convert.ToInt32(workItem.Revisions[i].Fields["External Link Count"].Value);
if (currCount > prevCount)
{
for (int j = prevCount;
j < (externalLinkColl.Count - (externalLinkColl.Count - currCount)); j++)
{
gvAllChanges.Rows.Add(workItem.Revisions[i].Fields["Changed By"].Value.ToString(),
Convert.ToDateTime(workItem.Revisions[i].Fields["Changed Date"].Value),
"External Link", "", externalLinkColl[j].ArtifactLinkType.Name + ":" +
externalLinkColl[j].LinkedArtifactUri, externalLinkColl[j].Comment);
}
}
Attachments
int prevCount = Convert.ToInt32(workItem.Revisions[i - 1].Fields["Attached File Count"].Value);
int currCount = Convert.ToInt32(workItem.Revisions[i].Fields["Attached File Count"].Value);
if (currCount > prevCount)
{
AttachmentCollection attachementColl = workItem.Attachments;
for (int j = prevCount; j < (attachementColl.Count - (attachementColl.Count - currCount)); j++)
{
gvAllChanges.Rows.Add(workItem.Revisions[i].Fields["Changed By"].Value.ToString(),
Convert.ToDateTime(attachementColl[j].AttachedTime), "Attachment", "",
attachementColl[j].Name, attachementColl[j].Comment);
}
}
Step 5: Get WorkItem Links
'WorkItem.WorkItemLinkHistory
' collection holds list of WorkItem
links including Deleted Links. This is the main reason why we are using this collection instead of 'WorkItem.Links
' collection.
To identify deleted links, look for 'RemovedBy
' property. If it is not empty or not set as default, then those links are deleted. Fetch all the required information of that link and include it in your history grid list.
foreach (WorkItemLink link in workItem.WorkItemLinkHistory)
{
gvAllChanges.Rows.Add(link.AddedBy, Convert.ToDateTime(link.AddedDate),
"WorkItem Link Added", "", "WorkItem " +
link.TargetId + " (" + link.LinkTypeEnd.Name + ")",
link.Comment);
if (link.RemovedBy != "" && link.RemovedBy != "TFS Everyone")
gvAllChanges.Rows.Add(link.RemovedBy, Convert.ToDateTime(link.RemovedDate),
"WorkItem Link Deleted", "", "WorkItem " + link.TargetId +
" (" + link.LinkTypeEnd.Name + ")", link.Comment);
}
Step 6: Have Your Entire History Visualized
Since we have collected all history changes, let's put them together arranging date wise.
gvAllChanges.Sort(this.gvAllChanges.Columns["ChangedDateAllChanges"], ListSortDirection.Descending);
Now, you will able to see all changes as latest on top. Sample, outlook looks like below:
You, can export this grid in format you want and save it locally. I have exported it in Excel using Interop services (Browse Code for more details).
WorkItem history saved with different timestamp is a BIG help in Auditing and Tracking activities...
Have a happy TFS API programming !!