Larry Wall, the creator of the infamous Perl programming language, has once said that the greatest virtues of a good programmer are Laziness, Impatience and Hubris. To not mislead the lazy “computer science” students, allow me to define each virtues:
- Laziness — The quality that makes you go to great effort to reduce overall energy expenditure. It makes you write labor-saving programs that other people will find useful, and document what you wrote so you don’t have to answer so many questions about it. Hence, the first great virtue of a programmer. See also impatience and hubris.
- Impatience – The anger you feel when the computer is being lazy. This makes you write programs that don’t just react to your needs, but actually anticipate them. Or at least pretend to. Hence, the second great virtue of a programmer. See also laziness and hubris.
- Hubris – Excessive pride. Also the quality that makes you write (and maintain) programs that other people won’t want to say bad things about. Hence, the third great virtue of a programmer. See also laziness and impatience.
A few days ago, a tester at our company filled a track that contained multiple attachments (Screenshots, Error Logs, etc) to assist the developers on their investigation. As we are using Microsoft’s Team Foundation System for source control and bug tracking, the most obvious way to get these attachments is through TFS Web Access; Frankly speaking, this is the only “official” way to access the track (bug report) on our company.
I don’t know if its a lack of feature or what, but there is no way in TFS Web Access to download multiple attachments at once. Additionally, when downloading attachments that contains a space on their filename, TFS Web Access will automatically concatenate the filename and that will remove the file’s extension (EG: Screenshot of the bug.jpg will be renamed to Screenshot)… You still need to manually rename the file with the proper extension to open it. And because I’m lazy, I don’t want to log-in to TFS Web Access, type in the track ID, click on the attachments tab, click on the file(s) I want to download, browse for the location where to save the file, create a new folder for the track, click save, minimize my browser, open the download location and (unzip the attachment, it its zipped, which normally is) to just view a single screenshot.
Since I am a big believer of Wall’s three virtues, I created a tool that will automate those boring and repetitive tasks for me (and allow me to download *all* attachments from a work item + workaround the “spacing” bug). The tool is called WIF or Work Item Fetcher and it is currently in Closed-Limited Beta.
At first, I thought about parsing the web pages to download the attachments using HTML Sanitizers and Regular Expressions but then I remembered that only Chuck Norris can parse HTML using regular expressions! So I searched the web and found out about the TFS SDK! Go .NET! Go Laziness! Okay, enough chit-chat, the rest of the post will be dedicated to discussing how to use the TFS API to download attachments from TFS.
Step 1: Acquiring the needed libraries
To be honest, I don’t really know where to download the DLLs. I’ve searched and found nothing. What I did was I referenced the DLLs located at C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies\. The DLLs should be available after you download and install Team Explorer for Visual Studio 2005. For our purposes, you should reference the following DLLs:
- Microsoft.TeamFoundation.DLL
- Microsoft.TeamFoundation.Client.DLL
- Microsoft.TeamFoundation.WorkItemTracking.Client.DLL
You must then add the needed declarations like so:
using Microsoft.TeamFoundation; using Microsoft.TeamFoundation.Client; using Microsoft.TeamFoundation.WorkItemTracking.Client;
Compile your project to check.
Remember to always compile your project after each step.
Step 2: Connecting to your TFS server
If you want to do anything with your TFS SDK, the first thing that you should do is to connect to your TFS server. Connecting is straight-forward, but a little tricky if you need to provide your network credentials. First, lets assume that the program will run on a machine that is connected to the domain with the proper credentials (AKA straight-forward way):
const string TFSServer = "http://mytfsserver:8090"; using (TeamFoundationServer tfsConn = new TeamFoundationServer(TFSServer)) { try { tfsConn.EnsureAuthenticated(); } catch (Exception ex) { MessageBox.Show(ex.Message, "Cannot connect to TFS Server!", MessageBoxButtons.OK, MessageBoxIcon.Error); } }
A call to EnsureAuthenticated(); is needed to check if your connection is properly authenticated. If not, the method will throw an exception.
How about if you need to manually provide login credentials or your machine is not joined to a domain? Just use the NetCredential object from System.Net to pass your credentials:
const string TFSServer = "http://mytfsserver:8090"; NetworkCredential netCred = new NetworkCredential("Username", "Password", "Domain"); using (TeamFoundationServer tfsConn = new TeamFoundationServer(TFSServer, netCred)) { try { tfsConn.EnsureAuthenticated(); } catch (Exception ex) { MessageBox.Show(ex.Message, "Cannot connect to TFS Server!", MessageBoxButtons.OK, MessageBoxIcon.Error); } }
Step 3: Acquire the Work Item
Now that we’re good and connected to TFS, our next task is to acquire the Work Item from TFS. First, we’ll need to acquire the WorkItemStore service from TFS by calling the GetService method:
WorkItemStore wis = (WorkItemStore)tfsServer.GetService(typeof(WorkItemStore));
Then, we’ll get the Work Item by invoking the GetWorkItem method:
WorkItem wi = wis.GetWorkItem(1000); //Where 1000 is the Work Item ID
Now, your code should look something like this:
const string TFSServer = "http://mytfsserver:8090"; NetworkCredential netCred = new NetworkCredential("Username", "Password", "Domain"); using (TeamFoundationServer tfsConn = new TeamFoundationServer(TFSServer, netCred)) { try { tfsConn.EnsureAuthenticated(); WorkItemStore wis = (WorkItemStore)tfsServer.GetService(typeof(WorkItemStore)); WorkItem wi = wis.GetWorkItem(1000); //Where 1000 is the Work Item ID } catch (Exception ex) { MessageBox.Show(ex.Message, "Something has gone wrong!", MessageBoxButtons.OK, MessageBoxIcon.Error); } }
Step 4: Get the attachments’ URI
After acquiring the Work Item, we can now directly iterate through the attachment collection to access its attachment’s URI:
foreach(Attachment attachment in wi.Attachments) { string FileName = attachment.Name; string attachmentURI = attachment.Uri; }
Cool huh? Just like that! Your entire code should look like this:
const string TFSServer = "http://mytfsserver:8090"; NetworkCredential netCred = new NetworkCredential("Username", "Password", "Domain"); using (TeamFoundationServer tfsConn = new TeamFoundationServer(TFSServer, netCred)) { try { tfsConn.EnsureAuthenticated(); WorkItemStore wis = (WorkItemStore)tfsServer.GetService(typeof(WorkItemStore)); WorkItem wi = wis.GetWorkItem(1000); //Where 1000 is the Work Item ID foreach(Attachment attachment in wi.Attachments) { string FileName = attachment.Name; string attachmentURI = attachment.Uri; } } catch (Exception ex) { MessageBox.Show(ex.Message, "Something has gone wrong!", MessageBoxButtons.OK, MessageBoxIcon.Error); } }
Step 5: Downloading the attachments
And now that we have the attachment’s URI, all there is left to do is to download it. Thankfully, .NET provided us with the WebClient class that simplifies the downloading:
using (WebClient wClient = new WebClient()) { wClient.Credentials = netCred; //Provide your network credentials. wClient.DownloadFile(attachment.Uri, Path.Combine(@"C:", attachment.Name)); }
That’s it! You’re done.
The completed code should look like this:
using (TeamFoundationServer tfsConn = new TeamFoundationServer(TFSServer, netCred)) { try { tfsConn.EnsureAuthenticated(); WorkItemStore wis = (WorkItemStore)tfsServer.GetService(typeof(WorkItemStore)); WorkItem wi = wis.GetWorkItem(1000); //Where 1000 is the Work Item ID foreach(Attachment attachment in wi.Attachments) { string FileName = attachment.Name; string attachmentURI = attachment.Uri; using (WebClient wClient = new WebClient()) { wClient.Credentials = netCred; wClient.DownloadFile(attachmentURI, Path.Combine(@"C:", FileName)); } } } catch (Exception ex) { MessageBox.Show(ex.Message, "Something has gone wrong!", MessageBoxButtons.OK, MessageBoxIcon.Error); } }
Step 6: Optional Improvements
The code above is the minimum required to have a fully functional attachment downloader for TFS. There are a couple of things that you can do to improve your program:
- Automatically create a directory for each Work Item. This way, you will have an organized local storage of the attachments.
- Instead of using the DownloadFile method to download your file, you can use the DownloadFileAsync method to download the file asynchronously. DownloadFileAsync is non-thread blocking. This call will also allow you to listen to EventHandlers for the download progress.
- After the download, you can check if the file is a zip file (by using Path.GetExtension), and if it is, you can use SharpZipLib to automatically unzip the package.
- Sometimes, instead of providing the Incident number, your users will provide the Incident Fix number. Its good to anticipate that and provide a way to resolve the Incident Number from the Incident Fix. To do that, first check if your user provided an Incident Fix by calling wi.Fields.GetById(25).Value.ToString() == “Incident Fix” then if yes, you can get the Incident Number by calling int ParentID = (int)wi.Fields.GetById(10120).Value. Thank you Impatience!
I can’t really share my program with you guys (you know all of that “corporate” legal mumbo-jumbo). But here’s the screenshot:
Thank you Laziness for making me go to great effort to reduce the total effort and time needed to download attachments from TFS. Thank you for making me write labor-saving programs that other people will hopefully find useful.
Thank you Impatience for pissing me off when I am manually downloading and renaming attachments one by one. Thank you too for making me angry when my initial prototype failed when I gave it an Incident Fix number rather than an Incident number.
And, as always, thank you Hubris!
(People around me knows exactly why!)































































This artical is great, it helped me a lot. I am building an automation testing software for our company, which including features to work with bugs in TFS.
BTW, Is there any way to view a Bug (type a bug ID) through Visual Studio IDE programatically? I have googled it, but no useable information found.
Thanks in advance.
What do you mean by “through Visual Studio IDE programatically”? You can look into creating a VS add-in for TFS.
Sorry about the unclear description. Here is the thing, I wish to build a desktop app, which can query bugs with a bug id directly from TFS, and if I choose to open it, then the app launch Visual Studio and open the bug in its original Bug editor UI, Is there any chance to do that?
Thanks for your reply again:)
It’s basically the same.
Thanks Ian, I’ll give it a try.
Wow, awesome. Just like you, I am lazy too and will go through great lengths to make things easier. At my work, we monitor back end processes through our scheduling software Control-M. We are tasked to identify if a process is running longer than the average run time then escalate to find out if it is hanging. This is done by getting the start time, average run time through the software then identify if it is already hanging. I hate computing time on my head. On top of that, there is a lag between each operation you do in Control-M. That’s why I wrote a Unix script to automate monitoring, calculating of long running jobs. Now all I do is stare at the monitor all day without lifting a finger.
Invoking your Hubris. Your comment system pisses me off. I can’t use the arrow keys nor copy paste. I had to manually type a URL!