Tfs Js Extension using TypeScript

Before I start I just want to give credit to Tiago Pascal for his help in getting me started with some of the basics and tips and tricks that got me start with the JavaScript extensions and to Alexander Vanwynsberghe for a debugging tip that makes this all a lot easier.

This post will give you a basic starting point for creating TFS Web Access JavaScript extension with most of it in proper TypeScript. This post will give a brief overview of the whole process and supply enough to get you started. In later posts I will go into the different sections in more detail and explain what bits of code do or how I found I was able to know that bits of code could work in the web access.

Files Required

First you need a name for your extension, I have choose to use B1n4ryD1g1t.Tfs.Extensions so when ever you see that you will replace it with your name but I will try remember to point out the couple of places required.

The easiest way I find to create these plugins is to create a new HTML Application with TypeScript, this provides a good starting point and also makes it more familiar to me as it’s a solution and will allow me to build the project and any TypeScript errors will be revealed to me through the normal error list window.


Create 2 files (and remember to do the name replacing here Smile), manifest.xml and B1n4ryD1g1t.Tfs.Extensions.min.js and then rename the solutions current app.ts to be B1n4ryD1g1t.Tfs.Extensions.debug.ts. If you build the solution and also show all files in the solution folder you will notice that there is now a B1n4ryD1g1t.Tfs.Extensions.debug.js and B1n4ryD1g1t.Tfs.Extensions.debug.js.map file, you can include those in your solution. The last step of setting up the solution is to remove the app.css, default.htm and web.config as we won’t be needing these.

Next you need to use NuGet to add a reference to JQuery to your project


Now right click on your jquery-{version}.js file and click on Search for TypeScript Typings… and install the jquery.TypeScript.DefinitelyTyped typing.


Your solution should end up looking something like below, we aren’t going to physically reference these jquery files in our extension they were purely used to get the typings.


What goes into the the manifest.xml

The mainifest.xml has the basic information for your extension that TFS will show on the extensions page in the web access.


The plugin node is self explanatory and for now you will leave the module nodes intact (remember to replace the name though). The 2 modules below allow our extension to run on the task and portfolio boards.

   1: <WebAccess version="12.0">

   2:   <plugin name="B1n4ryD1g1t Tfs Extensions - Web Access" vendor="Gordon Beeming" moreinfo="http://gbeeming.wordpress.com" version="1.7">

   3:     <modules>

   4:       <module namespace="B1n4ryD1g1t.Tfs.Extensions" loadAfter="TFS.Agile.TaskBoard.View"/>

   5:       <module namespace="B1n4ryD1g1t.Tfs.Extensions" loadAfter="TFS.Agile.Boards.Controls"/>

   6:     </modules>

   7:   </plugin>

   8: </WebAccess>

What is the minimum needed for the B1n4ryD1g1t.Tfs.Extensions.debug.ts?

The minimum content for our extension will be plain Js and will leave 4 errors in our solution that we can ignore


The content for the extension will look like below

   1: /// <reference path="Scripts/typings/jquery/jquery.d.ts" />

   2: var __extends = this.__extends || function (d, b) {

   3:     function __() { this.constructor = d; }

   4:     __.prototype = b.prototype;

   5:     d.prototype = new __();

   6: };

   7: define(["require", "exports", "Presentation/Scripts/TFS/TFS", "Presentation/Scripts/TFS/TFS.Core", "Presentation/Scripts/TFS/TFS.OM", "Presentation/Scripts/TFS/TFS.UI.Controls", "WorkItemTracking/Scripts/TFS.WorkItemTracking"],

   8: function (require1, exports, tfs, core, tfsOM, tfsUiControls, tfsWorkItemTracking) {

   9:     var TFS = tfs;

  10:     var Core = core;

  11:     var TFS_OM = tfsOM;

  12:     var TFS_UI_Controls = tfsUiControls;

  13:     var TFS_WorkItemTracking = tfsWorkItemTracking;

  14:     var B1n4ryD1g1tTfsExtension = (function (_super) {

  15:         __extends(B1n4ryD1g1tTfsExtension, _super);

  16:         function B1n4ryD1g1tTfsExtension(options) {

  17:             _super.call(this, options);

  18:         }

  19:         B1n4ryD1g1tTfsExtension.prototype.initializeOptions = function (options) {

  20:             _super.prototype.initializeOptions.call(this, $.extend({

  21:             }, options));

  22:         };

  23:         B1n4ryD1g1tTfsExtension.prototype.initialize = function () {

  24:             alert('this is running');

  25:         };

  26:         B1n4ryD1g1tTfsExtension._typeName = "B1n4ryD1g1t.Tfs.Extensions";

  27:         return B1n4ryD1g1tTfsExtension;

  28:     })(TFS_UI_Controls.BaseControl);

  29:     TFS.initClassPrototype(B1n4ryD1g1tTfsExtension, {});

  30:     TFS_UI_Controls.Enhancement.registerEnhancement(B1n4ryD1g1tTfsExtension, ".taskboard");

  31:     TFS_UI_Controls.Enhancement.registerEnhancement(B1n4ryD1g1tTfsExtension, ".agile-board");

  32: });

Add the extension to TFS Web Access

Open your debug.js file and copy all of its contents to the min.js file and minify it, the web essentials extension for Visual Studio will help with this as it’s as easy as ctrl + alt + x. Open the folder where your extension is and zip the debug.js, min.js and mainifest.xml files.


Now you will need to upload your extension into TFS. Browse to the TFS Server Home, click on the admin settings button and then on extensions tab.Click Install, browse for your newly created zip file and click ok. Now all you need to do it click enable and ok to the warning. Now just refresh any of the task boards or portfolio boards and you will see an alert from inside the extension.


Making changes to the extension

As you can see this is a very lengthy process to add the extension and none of that changes for checking for changes unless you use a trick that Alexander blogged about Debugging TFS Web Access Extensions. This will basically using fiddler tell your machine that when it requests the js file for your extension it must use the version js file that is in your solution instead of the one from TFS, this will speed up development a lot as you can just save the file which will trigger Visual Studio to update the debug.js file which refreshing TFS will now be loaded.

For the Editor I used the values

regex:http://labtfs01/_static/tfs/12/_scripts/TFS/.+//_plugins/.+/B1n4ryD1g1t.Tfs.Extensions.js with header:CachControl=no-cache and regex:http://labtfs01/_static/tfs/12/_scripts/TFS/.+//_plugins/.+/B1n4ryD1g1t.Tfs.Extensions.js with C:crAppsTfsJsExtensionsTfsJsExtensionsB1n4ryD1g1t.Tfs.Extensions.debug.js. Similar to the blog post just adding a wild card for the debug/min difference in TFS.

Adding some Real TypeScript

To the bottom of you .ts file add the TypeScript code below (sorry about no full TS highlighting).

   1: module B1n4ryD1g1tModule {

   2:     export class Core {

   3:         Require: any;

   4:         Exports: any;

   5:         TFS: any;

   6:         Core: any;

   7:         TFS_OM: any;

   8:         TFS_WorkItemTracking: any;

   9:         WorkItemManager: any;

  10:         CurrentlyFetchingWorkItems: boolean;

  11:         constructor(require1, exports, tfs, core, tfsom, tfsWorkItemTracking) {

  12:             this.Require = require1;

  13:             this.Exports = exports;

  14:             this.TFS = tfs;

  15:             this.Core = core;

  16:             this.TFS_OM = tfsom;

  17:             this.TFS_WorkItemTracking = tfsWorkItemTracking;

  18:         }

  19:         public init(): any {

  20:             this.initWorkItemManagerEvents();

  21:             var that = this;

  22:             window.setTimeout(function () {

  23:                 if (that.isAgileBoard()) {

  24:                     that.setAgileBoardIDs();

  25:                 }

  26:                 if (that.isTaskBoard()) {

  27:                     that.setTaskBoardIDs();

  28:                 }

  29:             }, 100);

  30:         }

  31:         private getCurrentTeamName(): string {

  32:             return this.TFS.Host.TfsContext.getDefault().currentTeam.name;

  33:         }

  34:         private isTaskBoard(): boolean {

  35:             return $(".taskboard").length > 0;

  36:         }

  37:         private isAgileBoard(): boolean {

  38:             return $(".agile-board").length > 0;

  39:         }

  40:         private setAgileBoardIDs(): void {

  41:             var idsToFetch = [];

  42:             $(".board-tile").each(function () {

  43:                 var id = $(this).attr("data-item-id");

  44:                 idsToFetch.push(parseInt(id));

  45:             });

  46:             this.loadWorkItems(idsToFetch, this.setAgileBoardIDsWork);

  47:         }

  48:         private setAgileBoardIDsWork(index, row, that: Core): void {

  49:             that.workWithAgileBoard(row[0], that);

  50:         }

  51:         private workWithAgileBoard(id, that: Core): void {

  52:             if (that.workWithAgileBoard_WorkItem(id)) {

  53:             }

  54:         }

  55:         private setTaskBoardIDs(): void {

  56:             var idsToFetch = [];

  57:             $("#taskboard-table .tbTile").each(function () {

  58:                 var id = $(this).attr("id");

  59:                 id = id.split('-')[1];

  60:                 idsToFetch.push(parseInt(id));

  61:             });

  62:             $("#taskboard-table .taskboard-row .taskboard-parent").each(function () {

  63:                 var id = $(this).attr("id");

  64:                 if (id != undefined) {

  65:                     id = id.split('_')[1];

  66:                     id = id.substring(1);

  67:                     idsToFetch.push(parseInt(id));

  68:                 }

  69:             });

  70:             this.loadWorkItems(idsToFetch, this.setTaskBoardIDsWork);

  71:         }

  72:         private setTaskBoardIDsWork(index, row, that: Core): void {

  73:             if (that.workWithTaskBoard(row[0], row[1], that)) {

  74:             }

  75:         }

  76:         private workWithTaskBoard(id, state, that: Core): void {

  77:             if (that.workWithTaskBoard_Task(id)) {

  78:             }

  79:             if (that.workWithTaskBoard_Requirement(id)) {

  80:                 that.taskboard_setRequirementState(id, state);

  81:             }

  82:         }

  83:         private workWithTaskBoard_Requirement(id): boolean {

  84:             return this.boards_setID("taskboard-table_p" + id, id);

  85:         }

  86:         private workWithTaskBoard_Task(id): boolean {

  87:             return this.boards_setID("tile-" + id, id);

  88:         }

  89:         private workWithAgileBoard_WorkItem(id): boolean {

  90:             var titleObj = $(".board-tile[data-item-id='" + id + "'] .title");

  91:             if ($(titleObj).length > 0 && $(titleObj).find(".TitleAdded").length == 0) {

  92:                 var idHtml = "<span class='TitleAdded' style='font-weight:bold;'>" + id + "</span> - ";

  93:                 $(titleObj).html(idHtml + $(titleObj).html());

  94:                 return true;

  95:             }

  96:             return false;

  97:         }

  98:         private boards_setID(idTagLookFor, id): boolean {

  99:             var titleObj = $("#" + idTagLookFor + " .witTitle");

 100:             if ($(titleObj).length > 0 && $(titleObj).find(".TitleAdded").length == 0) {

 101:                 var idHtml = "<span class='TitleAdded' style='font-weight:bold;'>" + id + "</span> - ";

 102:                 $(titleObj).html(idHtml + $(titleObj).html());

 103:                 return true;

 104:             }

 105:             return false;

 106:         }

 107:         private taskboard_setRequirementState(id, state): boolean {

 108:             var titleObj = $("#taskboard-table_p" + id + " .witTitle");

 109:             if ($(titleObj).length > 0 && $(titleObj).find(".StateAdded").length == 0) {

 110:                 var stateHtml = "<br/><br/><span class='StateAdded' style='color:#505050;font-size:smaller;font-weight:bold;'>" + state + "</span>";

 111:                 $(titleObj).html($(titleObj).html() + stateHtml);

 112:                 return true;

 113:             }

 114:             return false;

 115:         }

 116:         private workItemChanged(sender, workItemChangedArgs): void {

 117:             if (workItemChangedArgs.change === this.TFS_WorkItemTracking.WorkItemChangeType.Reset || workItemChangedArgs.change === this.TFS_WorkItemTracking.WorkItemChangeType.SaveCompleted) {

 118:                 var that = this;

 119:                 var id = workItemChangedArgs.workItem.id;

 120:                 var state = workItemChangedArgs.workItem.getFieldValue("System.State");

 121:                 if (that.isTaskBoard()) {

 122:                     window.setTimeout(function () {

 123:                         that.workWithTaskBoard(id, state, that);

 124:                     }, 100);

 125:                 } else if (that.isAgileBoard()) {

 126:                     window.setTimeout(function () {

 127:                         that.workWithAgileBoard(id, that);

 128:                     }, 100);

 129:                 }

 130:             }

 131:         }

 132:         private loadWorkItems(idsToFetch: Array, onComplete): void {

 133:             var that = this;

 134:             that.loadWorkItemsWork(idsToFetch, onComplete, that);

 135:         }

 136:         private loadWorkItemsWork(idsToFetch: Array, onComplete, that: Core): void {

 137:             var takeAmount = 100;

 138:             if (takeAmount >= idsToFetch.length) {

 139:                 takeAmount = idsToFetch.length;

 140:             }

 141:             if (takeAmount > 0) {

 142:                 that.WorkItemManager.store.beginPageWorkItems(idsToFetch.splice(0, takeAmount), [

 143:                     "System.Id",

 144:                     "System.State"

 145:                 ], function (payload) {

 146:                         that.loadWorkItemsWork(idsToFetch, onComplete, that);

 147:                         $.each(payload.rows, function (index, row) {

 148:                             onComplete(index, row, that);

 149:                         });

 150:                     }, function (err) {

 151:                         that.loadWorkItemsWork(idsToFetch, onComplete, that);

 152:                         alert(err);

 153:                     });

 154:             }

 155:         }

 156:         private initWorkItemManagerEvents(): void {

 157:             var service = this.TFS_OM.TfsTeamProjectCollection.getDefaultConnection().getService(this.TFS_WorkItemTracking.WorkItemStore);

 158:             this.WorkItemManager = service.workItemManager;

 159:             var that = this;

 160:             this.WorkItemManager.attachWorkItemChanged(function (sender, workItemChangedArgs) {

 161:                 that.workItemChanged(sender, workItemChangedArgs);

 162:             });

 163:         }

 164:     }

 165: }

This is all the code we will need to do the magic, all that is left is to wire it up to the extension. This can be done by replacing the method below in the original snippet for the extension.

   1: B1n4ryD1g1tTfsExtension.prototype.initialize = function () {

   2:     var bdCore = new B1n4ryD1g1tModule.Core(require1, exports, TFS, Core, TFS_OM, TFS_WorkItemTracking);

   3:     bdCore.init();

   4: };

If you fresh your page now and you have the debug tip running you will see that you have IDs on your work items and on the task board the requirement states are showing.





Hope this sparks something in others as it has in me, As said in the intro I will be extending on what is covered in this article in later posts to provide more details.


Solution – TfsJsExtensions_2013-09-27 06-55-45Z.zip

Usable Web Extension


Running as Administrator with Click Once Application on Windows 8

You have probably noticed that with windows 8 when you disable UAC it doesn’t fully turn off. You are able to completely turn it off but that then disables the ability to use any of the Modern UI applications. So today I dug a bit and eventually managed to get a solution to make your application run as admin when being run from a click once deploy.

The way a user would be able to enable admin mode for a regular application is to Right click on the application and click ‘Run as administrator’


With click once applications you aren’t able to do this. I suppose you could find one of the many ways to locate the actual executable and then run as administrator but then every time the application updates you will need to locate the application again and also many users (including myself) won’t see this as a suitable way to launch an application as administrator.

After trying all the regular methods of enabling an application to run as full administrator I eventually got to the solution below.

Add these usings into the program.cs file

   1: using System.ComponentModel;

   2: using System.Linq;

Add the member and method to the program.cs class

   1: private const uint BCM_SETSHIELD = 0x160C;


   3: [DllImport("user32", CharSet = CharSet.Auto, SetLastError = true)]

   4: private static extern int SendMessage(IntPtr hWnd, uint Msg, int wParam, IntPtr lParam);

Add the parameter string[] args to the main method if it does not already exist

   1: public static void Main(string[] args)

And finally wrap all the code inside you main method with

   1: if (string.IsNullOrEmpty((from o in args where o == "--engage" select o).FirstOrDefault()))

   2: {

   3:     var btnElevate = new Button();

   4:     btnElevate.FlatStyle = FlatStyle.System;


   6:     SendMessage(btnElevate.Handle, BCM_SETSHIELD, 0, (IntPtr) 1);


   8:     var processInfo = new ProcessStartInfo();

   9:     processInfo.Verb = "runas";

  10:     processInfo.FileName = Application.ExecutablePath;

  11:     processInfo.Arguments = string.Join(" ", args.Concat(new[] { "--engage" }).ToArray());

  12:     try

  13:     {

  14:         Process p = Process.Start(processInfo);

  15:         p.WaitForExit();

  16:     }

  17:     catch (Win32Exception)

  18:     {

  19:         //Do nothing. Probably the user cancelled the UAC window or provided invalid credentials.

  20:     }


  22:     Application.Exit();

  23: }

  24: else

  25: {

  26:     // place code that was in the main method here

  27: }

That’s all you need to be able to run your application as administrator on launch. Basically what is going to happen is your application will start up and see that there is no command argument for –engage, it will then get it’s own executable path and attempt to run itself again using administrator mode. If a user has UAC enabled they will be prompted as usual to allow the application to run in admin mode and if they have UAC disable in windows 8 the application will now run if real administrator mode.


Important Notice to Microsoft Tag Customers

Email From Microsoft

Important Notice to our Microsoft Tag Customers

To our valued Microsoft Tag Customer,

This August 19, 2013 notice is to inform you that the Microsoft Tag service will terminate in two years, on August 19, 2015.  We are providing this two year termination notice in accordance with our Terms of Use for the Microsoft Tag Service located at this link: http://tag.microsoft.com/tag-terms-of-use.aspx See Section 2 – Availability of Service; Changes to the Agreement & Service, paragraph 2.1.

Through August 19, 2015, you will be able to continue to log into your existing Microsoft Tag service account, use existing Microsoft Tag codes, generate new Microsoft Tags, and run reports as usual.

To help you prepare for the termination of the Microsoft Tag service on August 19, 2015, Scanbuy has been selected to support Microsoft Tag technology on the ScanLife platform beginning no later than September 18th, 2013, and to offer transition and migration services to Microsoft TAG customers who choose to migrate to the ScanLife platform.  This transition path will help you to continue running your campaigns using Microsoft Tags on the ScanLife platform.

Scanbuy is the largest provider of QR codes and runs ScanLife, a cloud-based mobile engagement platform for creating personalized, uniquely tailored experiences for consumers to digitally engage with brands in their everyday surroundings through smartphones.

If you wish to learn more about the ScanLife platform you may contact Adam Gold, VP of Sales at adamg@scanbuy.com or call 212-278-0178 x 400.

We thank you for allowing us to serve you as our Microsoft Tag customer.  If you have questions that Microsoft can assist you with please contact taginfo@microsoft.com.


Eric Engstrom,     
General Manager, Microsoft


Embed images in mail with Outlook 2013 signature

Have you recently tried embedding an image into your signature where you would normally in older outlook versions create a signature in html format and then use that as your signature.

As you know this worked as expected in the older version and now in 2013 it doesn’t work anymore.

The simple way to fix this is to add an entry into your registry using the below inside a .reg file.


   2: Windows Registry Editor Version 5.00 

   3: [HKEY_CURRENT_USERSoftwareMicrosoftOffice15.0OutlookOptionsMail]

   4: "Send 

   5: Pictures With Document"=dword:00000001 


That’s all you need, restart outlook and now when you send an email the images will be embedded into the mail.


Embed images in mail with Outlook signature

Have you recently tried embedding an image into your signature where you would normally in older outlook versions create a signature in html format and then use that as your signature.

As you know this worked as expected in the older version and now in 2013 it doesn’t work anymore.

The simple way to fix this is to add an entry into your registry using the below inside a .reg file.


   2: Windows Registry Editor Version 5.00 

   3: [HKEY_CURRENT_USER\Software\Microsoft\Office\15.0\Outlook\Options\Mail]

   4: "Send 

   5: Pictures With Document"=dword:00000001 


That’s all you need, restart outlook and now when you send an email the images will be embedded into the mail.


The type caught or thrown must be derived from System.Exception

Today I received a random error, the error message read “The type caught or thrown must be derived from System.Exception”. The reason why this was to me a random error was because I was trying to catch a Microsoft.TeamFoundation.WorkItemTracking.Client.ServerRejectedChangesException exception.


To try see if maybe this was a bug or maybe my pc needed a reboot or something I started drilling into the definitions of the exception to try get all the way through to System.Exception.

basically this looked like below

public class ServerRejectedChangesException : ValidationException


public class ValidationException : ClientException


public class ClientException : TeamFoundationServerException


public class TeamFoundationServerException : Microsoft.VisualStudio.Services.Common.VssException

At the TeamFoundationServerException class I noticed that the VssException was not lit up by Visual Studio which to me meant that I didn’t have a reference added to be able to drill into it’s definition like I was for the previous levels.

I added a reference to Microsoft.VisualStudio.Services.Common and suddenly the error been thrown when trying to build my project went away. Basically this allowed the IDE to navigate through to System.Exception like below.

public abstract class VssException : ApplicationException


public class ApplicationException : Exception


public class Exception : ISerializable, _Exception

It would be cool if this extra reference was not needed but I understand why it is Smile.


New site location

I am moving back to a hosted wordpress.com blog so that I can adventure more with the azure tools and features and not worry about my blog switching off.


You can find any new content on the new site http://gbeeming.wordpress.com.


Visual Studio 2013 Preview Update

There is an update for the preview of Visual Studio 2013, this was release a couple days ago. There are 2 ways to get the update.

The first look at the notifications in VS


The other way is to browse to Microsoft Download Center using the link below


Once you have downloaded the exe file, the installation is the same as the updates for VS 2012, also remember if you are wanting to install this update on multiple machines you can download all the files using the /layout command argument (see below) and then copy the files to each machine.

Open cmd.exe and navigate to the folder where you downloaded the exe update file to, then run “VS2013 Preview Update.exe” /layout


This will launch the update application prompting for a location to download the installation files to. Next Click Download and wait until the download completes.

image image image

Now that the files are complete launch the installer again without /layout and then the installer will use the downloaded files instead of downloading each file each time you run the installer.

This method can be used with all VS bootstrap installers and is not only a VS 2013 installer feature.


TfsApi 1.2

Documentation for this release can be found on the link http://documentation.azurewebsites.net/TfsApi/1.2/Default.aspx.

Features adding to this release


  • Area Path Management
  • Team Management

This will probably be the last update for a while but feel free to contact me for additions that you would like.


The Release can be found using the below link on codeplex