Create your first extension with Visual Studio

Last Update: 9/26/2016
Need help? Contact the engineering team directly.

Extensions enable you to create first-class integration experiences within Visual Studio Team Services. An extension can be a simple context menu or toolbar action or it can be a complex and powerful custom UI experience that light up within the account, collection, or project hubs.

Get started now by creating your own hub that displays the results a query, and an action on the queries context menu to launch your hub.

hub

In this page:

Create a hub

Use a hub to surface your web app in an iframe in Visual Studio Team Services. The one we're creating here will show up in the team project's Work hub group.

Location of a new hub in Visual Studio Team Services

Create the web app

Start by creating the web app with the page that will be your hub.

  1. In Visual Studio, create a new web site.

    File menu, new web site

  2. Use the ASP.NET Empty Web Site template.

    New project dialog with ASP.NET Web Application selected

  1. Get the Client SDK VSS.SDK.js file and add it to your web app. Place it in the home/sdk/scripts folder.

    1. Use the 'npm install' command to retrieve the SDK: npm install vss-web-extension-sdk.
    2. To learn more about the SDK, visit the Client SDK Github Page.
  2. Add the web page that you want to display as a hub. We're doing a simple hello-world.html page here, added to the home directory.

  3. In your HTML page, add a reference to the SDK, and call init() and notifyLoadSucceeded().

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Hello World</title>
        <script src="sdk/scripts/VSS.SDK.js"></script>
    </head>
    <body>
        <script type="text/javascript">VSS.init();</script>
        <h1>Hello World</h1>
        <script type="text/javascript">VSS.notifyLoadSucceeded();</script>
    </body>
    </html>
    
  1. Enable SSL.

    Enable SSL

  1. If you like, you can add a square image in the images folder that identifies your extension. We'll display it when someone installs your extension.

    installed extension

    You don't need to do this, though for your extension to work.

Create the extension manifest

The extension manifest tells Visual Studio Team Services about your extension.

  1. Create a json file (vss-extension.json, for example) in the home directory of your web app to describe your extension.

    {
    "manifestVersion": 1,
    "id": "sample-extension",
    "version": "0.1.0",
    "name": "My first sample extension",
    "description": "A sample Visual Studio Services extension.",
        "publisher": "fabrikam",
        "targets": [
            {
                "id": "Microsoft.VisualStudio.Services"
            }
        ]
    }
    
  2. Specify the path to your extension's icon in your manifest. If you want to skip this step for now, that's fine. The extension will function without the icon.

    {
        ...
        "icons": {
            "default": "images/logo.png"
        }
    }
    
  1. Add your contribution - the Hello hub - to your extension manifest.

    Include the scopes that your extension requires. In this case, we need vso.work to access work items.

    {
        ...
        "contributions": [
            {
                "id": "Fabrikam.HelloWorld",
                "type": "ms.vss-web.hub",
                "description": "Adds a 'Hello' hub to the Work hub group.",
                "targets": [
                    "ms.vss-work-web.work-hub-group"
                ],
                "properties": {
                    "name": "Hello",
                    "order": 99,
                    "uri": "hello-world.html"
                }
            }
        ],
        "scopes": [
            "vso.work"
        ]
    }
    

    For each contribution in your extension, the manifest defines

    • the type of contribution (hub in this case),
    • the contribution target (the work hub group),
    • and the properties that are specific to each type of contribution. For a hub, we have

      Property Description
      name name of the hub
      order placement of the hub in the hub group
      uri path (relative to the extension's baseUri) of the page to surface as the hub
  2. Add the files that you want to include in your package - your HTML page, your scripts, the SDK script and your logo.

    {
        ...
        "files": [
            {
                "path": "hello-world.html", "addressable": true
            },
            {
                "path": "scripts", "addressable": true
            },
            {
                "path": "sdk/scripts", "addressable": true
            },
            {
                "path": "images/logo.png", "addressable": true
            }
        ]
    }
    

    Set addressable to true unless you include other files that don't need to be URL-addressable.

Package and publish your extension

Packaging and publishing

Install your extension

  1. From your account control panel (https://{account}.visualstudio.com/_admin/_ext), go to the project collection administraton page.

    Control panel, view the collection administration page link

  2. In the Extensions tab, find your extension in the "Extensions Shared With Me" group and install it.

    Control panel, Extensions tab, Install button

If you can't see the Extensions tab, make sure you're in the control panel (the project collection level administration page - https://{account}.visualstudio.com/_admin/) and not the administration page for a team project.

If you're on the control panel, and you don't see the Extensions tab, extensions may not be enabled for your account. You can get early access to the extensions feature by joining the Visual Studio Partner Program.

Try out your extension

  1. Enable https for your web app.

    Properties dialog with SSL enabled

  2. Start your app in Visual Studio so that Visual Studio Team Services can access it.

  3. Go to your hub in the Work hub group.

    Hello hub in the Home hub group

Add a grid control

Now add a grid control to display some data in hello-world.html.

  1. Add a div element in the body of the page to contain the grid.

    <div id="grid-container"></div>
    
  2. In the script element, before calling VSS.notifyLoadSucceeded(), initialize the VSS SDK.

    // Sets up the initial handshake with the host frame
    VSS.init({
        // Our extension will explicitly notify the host when we're done loading
        explicitNotifyLoaded: true,
    
        // We are using some Team Services APIs, so we will need the module loader to load them in
        usePlatformScripts: true,
        usePlatformStyles: true       
    });
    
  3. Create a grid and load it with data. (Replace your current call to VSS.notifyLoadSucceeded() with the following snippet)

    // Load Team Services controls
    VSS.require(["VSS/Controls", "VSS/Controls/Grids"],
        function (Controls, Grids) {
    
        // Initialize the grid control with two colums, "key" and "value"
        var dataSource = [];
        dataSource.push({key: "key", value: "value"});
    
        Controls.create(Grids.Grid, $("#grid-container"), {
            height: "1000px", // Explicit height is required for a Grid control
            columns: [
                // text is the column header text. 
                // index is the key into the source object to find the data for this column
                // width is the width of the column, in pixels
                { text: "Property key", index: "key", width: 150 },
                { text: "Property value", index: "value", width: 600 }
            ],
            // This data source is rendered into the Grid columns defined above
            source: dataSource
        });
        VSS.notifyLoadSucceeded();
    });
    
  4. Refresh the page to see the grid.

    Grid control on the hello world page

Call a REST API

Call a REST API and display the results in the grid control.

  1. Get the client service. In this case, we're getting the work item tracking client.

    Change this:

    // Load VSTS controls
    VSS.require(["VSS/Controls", "VSS/Controls/Grids"],
        function (Controls, Grids) {
    

    to this:

    // Load VSTS controls and REST client
    VSS.require(["VSS/Controls", "VSS/Controls/Grids",
        "VSS/Service", "TFS/WorkItemTracking/RestClient"],
        function (Controls, Grids, VSS_Service, TFS_Wit_WebApi) {
    
        // Get a WIT client to make REST calls to VSTS
        var witClient = VSS_Service.getCollectionClient(TFS_Wit_WebApi.WorkItemTrackingHttpClient);
    
  2. Call the API (getWorkItems) using the client service (witClient), with a callback that loads the grid control with the results.

    Change this:

    // Initialize the grid control with two colums, "key" and "value"
    var dataSource = [];
    dataSource.push({key: "key", value: "value"});
    
    Controls.create(Grids.Grid, $("#grid-container"), {
        height: "1000px", // Explicit height is required for a Grid control
        columns: [
            // text is the column header text. 
            // index is the key into the source object to find the data for this column
            // width is the width of the column, in pixels
            { text: "Property key", index: "key", width: 150 },
            { text: "Property value", index: "value", width: 600 }
        ],
        // This data source is rendered into the Grid columns defined above
        source: dataSource
    });
    

    to this:

    // Call the "queryById" REST endpoint, giving a query ID
    witClient.getWorkItems(/* some work item IDs */ [1,2,3,4], ["System.Title"]).then(function(workItems) {
    
        // Create a Grid control to display the Work Items
        Controls.create(Grids.Grid, $("#grid-container"), {
            height: "1000px", // Explicit height is required for a Grid control
            columns: [
                // text is the column header text. 
                // index is the key into the source object to find the data for this column
                // width is the width of the column, in pixels
                { text: "Work Item ID", index: "id", width: 150 },
    
                // getColumnValue provides a mechanism for the grid to get the data for a cell
                // (at the given row number) if it is not directly keyed off the source object.
                { text: "Title", width: 150, getColumnValue: function(dataIndex) { 
                    // The Work Item's Title is at source[row].fields["System.Title"].
                    // this.getRowData(n) returns the nth item from the source object, so we 
                    // can return the "System.Title" property under fields.
                    return this.getRowData(dataIndex).fields["System.Title"]; 
                } },
                { text: "URL", index: "url", width: 600 }
            ],
            // This data source is rendered into the Grid columns defined above
            source: workItems
        }); 
    });
    

    This code assumes you have at least four work items in your team project. If you don't create them before you try the action.

  3. Refresh the page to see the data from your REST API call displayed in the grid.

    API results displayed in the grid

Add an action

You can add actions to the Visual Studio Team Services user interface that call your extension. In this case, you'll add an action to the context menu for queries and folders in the work hub that launches the Hello hub and send it a query to run.

See the contributions reference to see other places where you can contribute actions.

  1. Add your action to the contributions section of your extension manifest.

    "contributions": [
        {
            "id": "myAction",
            "type": "ms.vss-web.action",
            "description": "Run in Hello hub action",
            "targets": [
                "ms.vss-work-web.work-item-query-menu"
            ],
            "properties": {
                "text": "Run in Hello hub",
                "title": "Run in Hello hub",
                "icon": "images/icon.png",
                "groupId": "actions",
                "uri": "action.html"
            }
        }
    ]
    

    Property Description
    text Text that will appear on the menu item.
    title Tooltip text that will appear on the menu item.
    icon URL to an icon that will appear on the menu item. Relative URLs are resolved using baseUri.
    groupId Determines where this menu item will appear in relation to the others. How to discover menu group identifiers
    uri URI to a page that registers the menu action handler (see below).
    registeredObjectId (Optional) Name of the registered menu action handler. Defaults to the contribution id.

  2. Add an HTML page called action.html to your web app to handle your action.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Action Sample</title>
    </head>
    <body>
        <div>
            The end user doesn't see the content on this page.
            It is only in the background to handle the contributed menu item being clicked.
        </div>
    </body>
    </html>
    
  3. Register a handler object to handle your action. For now, just raise an alert.

    <script src="sdk/scripts/VSS.SDK.js"></script>
    <script>
         VSS.init();
    
         // Use an IIFE to create an object that satisfies the IContributedMenuSource contract
         var menuContributionHandler = (function () {
            "use strict";
            return {
                // This is a callback that gets invoked when a user clicks the newly contributed menu item
                // The actionContext parameter contains context data surrounding the circumstances of this
                // action getting invoked.
                execute: function (actionContext) {
                    alert("Hello, world");
                }
            };
        }());
    
        // Associate the menuContributionHandler object with the "myAction" menu contribution from the manifest.
        VSS.register("myAction", menuContributionHandler);
    </script>
    
  4. Install your extension and try it out. The action has been added to the context menu for queries and folders in the queries hub (work hub group).

    action in the context menu of a query

  1. Update the execute: block to open your Hello hub.

    execute: function (actionContext) {
    
        // Get the Web Context to create the uri
        var VSTSContext = VSS.getWebContext();
    
        // Navigate to the new View Assoicated Work Items hub.
        // Fabrikam is the extension's namespace and Fabrikam.HelloWorld is the hub's id.
        window.parent.location.href = VSTSContext.host.uri +
            vstsContext.project.name + "/_apps/hub/" +
            VSS.getExtensionContext().namespace + "/Fabrikam.HelloWorld";
    }
    

    Refresh the page and try it again. It opens the Hello hub.

  2. Update your action again to pass the selected item to your hub.

    window.parent.location.href = VSTSContext.host.uri +
        vstsContext.project.name + "/_apps/hub/" +
        VSS.getExtensionContext().namespace + "/Fabrikam.HelloWorld?queryId=" +
        actionContext.queryId;
    
  3. Update hello-world.html to run the query from the action context instead of getting a hardcoded set of work items.

    // Load VSTS controls and REST client
    VSS.require(["VSS/Controls", "VSS/Controls/Grids",
        "VSS/Service", "TFS/WorkItemTracking/RestClient"],
        function (Controls, Grids, VSS_Service, TFS_Wit_RestClient) {
    
        // Get a WIT client to make REST calls to VSTS
        var witClient = VSS_Service.getCollectionClient(TFS_Wit_RestClient.WorkItemTrackingHttpClient);
    
        // Call the "queryById" REST endpoint, giving a query ID
        witClient.queryById(location.search.substr("?queryId=".length)).then(function(queryResult) {
    
            // The query result returns a lit of shallow references to Work Items, so next we make
            // a REST call to get actual Work Items.
            var workItemIds = queryResult.workItems.map(function(reference) { return reference.id; });
    
            // The getWorkItems method takes a list of Work Item IDs and a list of fields to fetch.
            witClient.getWorkItems(workItemIds, ["System.Title"]).then(function(workItems) {
    
                // Create a Grid control to display the Work Items
                Controls.create(Grids.Grid, $("#grid-container"), {
    
                    // Explicit height is required for a Grid control
                    height: "500px",
                    columns: [
                        // text is the column header text. 
                        // index is the key into the source object to find the data for this column
                        // width is the width of the column, in pixels
                        { text: "Work Item ID", index: "id", width: 150 },
    
                        // getColumnValue provides a mechanism for the grid to get the data for a cell
                        // (at the given row number) if it is not directly keyed off the source object.
                        { text: "Title", width: 150, getColumnValue: function(rowNum) { 
                            // The Work Item's Title is at source[row].fields["System.Title"].
                            // this.getRowData(n) returns the nth item from the source object, so we 
                            // can return the "System.Title" property under fields.
                            return this.getRowData(rowNum).fields["System.Title"]; 
                        } },
                        { text: "URL", index: "url", width: 600 }
                    ],
                    // This data source is rendered into the Grid columns defined above
                    source: workItems
                }); 
    
                // Tells the host frame that the extension is done loading, which releases the UI to the user.
                VSS.notifyLoadSucceeded();
            });
        });
    });
    
  4. Run the action again from a query (not a folder) to see the results of that query in the Hello hub.

    Query results in the hello hub

    Now when you use your action, the results of the selected query will be displayed in your hub.

Deploy your extension to Microsoft Azure

  1. If you don't have an Azure subscription, create one. You can use the free trial.

  2. Create a web app in Microsoft Azure to host your extension.

    Microsoft Azure portal, create a web app

  3. Publish your web site from the solution explorer.

    Solution explorer, project context meny, publish web site

  4. Publish to Azure.

    Publish web dialog box

  5. Pick the web app that you set up to host your extension.

    Select existing web site dialog box with the web site selected

    If your web site doesn't show up, use the Manage subscriptions dialog to connect your Visual Studio account to your Microsoft Azure subscription.

  6. Publish your extension.

    Publish button on the Publish web dialog box

  7. Change your extension manifest to use your Microsoft Azure web app instead of localhost.

    "baseUri": "https://fabrikam-vso-extensions.azurewebsites.net/",
    
  8. Install your extension again and try it out.