Sommerurlaub auf Kauai in Hawaii — 10hourflight

Miriam’s blog (in German) about what we do when we are not working at MSFT:

Da wir so nah wohnen, dachten wir, fliegen wir mal nach Hawai’i. Im Mai war es endlich soweit – 10 Tage auf Kaua’i, eine der kleineren Insel des Staates Hawai’i im Norden. Da sonst nicht so viel passiert ist im Mai, hier ein ausführlicher Bericht!

via Sommerurlaub auf Kauai in Hawaii — 10hourflight

VSTS Tags MRU extension – Part 2

In the last post we ended up with a list of tags a user added last to a work item. The next step is now to keep an MRU list with tags from earlier sessions and update it when new tags are added.

Every time the user adds tags to a work item we want to merge these new tags with the (persisted) list of tags. We will keep a maximum of N tags and need to either only add tags to the list (if |tags| < N), or and and remove (if |tags| > N), or just reorder the tags in the list, so that the most recently used tags appear first in the final dropdown menu.

Keeping MRU list of tags

We want to store at maximum 5 tags for now, so we add a constant to our app.ts:

/** Maximum size of MRU */
const MAX_TAGS = 5;

And as before, we implement our business logic as a simple singleton:

class Tags {
  /** Key used for document service */
  private static KEY: string = "tags";
  private static instance: Tags = null;

  /** Get or create singleton instance */
  public static getInstance(): Tags {
    if (!Tags.instance) {
      Tags.instance = new Tags(MAX_TAGS);
    }

    return Tags.instance;
  }

  constructor(private maxCount: number) {
  }
}

This class needs to keep track of

  • what tags are currently in the MRU list
  • the order of tags

While we could maintain a dictionary depicting whether a tag is in the list and a queue with the MRU order, for only 5 tags a simple array is probably be enough.

private queue: string[] = [];

Now, when we add tag, and it is not in the queue yet, we just add it in front of the queue, or we delete it from its current position in the queue and then add it in front of the queue.

In order to maintain our maximum of 5 tags, we add another method prune, that removes tags from the end of the queue if the overall count is more than the configured max and call it

public addTag(tag: string) {
  // Remove tag from current position
  var idx = this.dict[tag];
  if (idx !== -1) {
    this.queue.splice(idx, 1);
  }

  // Add tag in first position and record position
  this.queue.unshift(tag);

  this.prune();
}

/** Ensure only maximum number of tags configured is stored */
private prune() {
  if (this.queue.length < this.maxCount) {
    for (var i = 0; i < this.queue.length - MAX_TAGS; ++i) {
      this.queue.pop();
    }
  }
}

For later displaying tags in the context menu, we can just return the current queue which contains our tags in the correct order

public getTags(): string[] {
  return this.queue;
}

Persisting Data from an extension

The VSTS framework provides the ExtensionDataService which allows us to store key/value pairs or collections of JSON documents on the VSTS servers. Usage is quite simple, to store a value we just need to get an instance of the service, and call setValue with a key and a value. The value can be as simple as a string, or an JS object that’s transparently serialized to JSON. We can also pass a scope, that limits values either to an “account” or a “user” scope:

VSS.getService(VSS.ServiceIds.ExtensionData).then((dataService) => {
  dataService.setValue("key", "value", { scopeType: "User" });
});

For the tags extension we want a user scope and our value to store will be an array of strings containing our tags.

Persisting Tags

First step is again to add another import to our app.ts file:

import VSS_Extension_Service = require("VSS/SDK/Services/ExtensionData");

To keep it simple, loading tags is something we’d like to do only once at the beginning of a session, and then save every time tags are added. This way, we might run into conflicts if the user is working with different browser tabs/windows at the same time, but for this sample that last-write-wins concurrency is enough.

Using the ExtensionDataService we can modify the getInstance call to retrieve the list of tags from the data service using getValue and add every one using the addTag method we implemented above. Since service calls use Promises, we change getInstance to return a promise instead of a value. If the instance has already been created, we use Q(<value>) to return an immediately resolved promise, otherwise we retrieve tags and then create the instance:

/** Get or create singleton instance */
public static getInstance(): IPromise<Tags> {
  if (Tags.instance) {
    return Q(Tags.instance);
  } else {
    return VSS.getService(VSS.ServiceIds.ExtensionData).then(
      (dataService: VSS_Extension_Service.ExtensionDataService) => {
      return dataService.getValue(Tags.KEY, {
        defaultValue: [],
        scopeType: "User"
      }).then((savedTags: string[]) => {
        Tags.instance = new Tags(MAX_TAGS);
        if (savedTags) {
          savedTags.forEach(t => Tags.instance.addTag(t));
        }

        return Tags.instance;
      });
    });
  }
}

Persisting tags will be done in another method, again getting a service instance (we could cache the instance), and calling then calling setValue. A promise is returned to allow callers to wait for the end of the call:

public persist(): IPromise<any> {
  return VSS.getService(VSS.ServiceIds.ExtensionData).then(
    (dataService: VSS_Extension_Service.ExtensionDataService) => {
      dataService.setValue(Tags.KEY, this.queue, {
        scopeType: "User"
      });
    });
}

Since we do want to show the tag context menu as fast as possible, we will proactively initialized that tag service as soon as the extension file is loaded:

// Proactively initialize instance and load tags
Tags.getInstance();

Showing Tags in work item context menu

We are nearly done,

// Register context menu action
VSS.register("tags-mru-work-item-menu", {
  getMenuItems: (context) => {
    return Tags.getInstance().then(tags => {
      var childItems: IContributedMenuItem[] = [];
      tags.getTags().forEach(tag => {
        childItems.push(<IContributedMenuItem>{
          text: tag,
          title: `Add tag: ${tag}`,
          action: () => {});
        });
      });

      if (childItems.length === 0) {
        childItems.push(<IContributedMenuItem>{
          title: "No tag added",
          disabled: true
        });
      }

      return [<IContributedMenuItem>{
        title: "Recent Tags",
          childItems: childItems
        }]
      });
   });
});

 

Updating work items

Final step is to add an action to the child menu items to actually add the tag to all selected work items. First we need to determine the selected work items. Unfortunately, the different VSTS views are not consistent in exposing the ids of selected work items right now. We need to look for different properties in the passed context depending on the view we are in. The logic is:

  • Backlog – array of numbers called workItemIds
  • Boards (Kanban/Iteration) – single number called id
  • Query results – array of numbers called ids

To unify in an array called ids we need to add the following code to the beginning of the getMenuItems method:

// Not all areas use the same format for passing work item ids.
// "ids" for Queries, "workItemIds" for backlogs, "id" for boards
var ids = context.ids || context.workItemIds;
if (!ids || context.id) {
  // Boards only support a single work item
  ids = [context.id];
}

Then, in the action handler of our child menu items, we need to:

  1. Get the work items
  2. For each work item
    1. Get the existing value for the System.Tags field
    2. Concatenate with the tag to add using “;” as separator
    3. Update work item

Since we are changing work items not opened in any form right now, we need to use the REST API for the update operations. Some additional imports are required:

import TFS_Wit_Contracts = require("TFS/WorkItemTracking/Contracts");
import TFS_Wit_Client = require("TFS/WorkItemTracking/RestClient");
import TFS_Wit_Services = require("TFS/WorkItemTracking/Services");

Then we just get an http client and start iterating over the work items:

// Get work items, add the new tag to the list of existing tags, then update
var client = TFS_Wit_Client.getClient();
client.getWorkItems(ids).then((workItems) => {
  for (var workItem of workItems) {
    client.updateWorkItem([{
      "op": "add",
      "path": "/fields/System.Tags",
      "value": (workItem.fields["System.Tags"] || "") + ";" + tag
    }], workItem.id);
  }
});

(Potential optimization would be to use the batch API for the work item updates instead of making a single call per work item)

All done

That’s it. The extension is done, can be published, and should mostly work as designed. I say mostly, because, if you remember, I mentioned earlier that there is one drawback, for which no workaround exists yet: When we add a tag, we need to use the REST API to update the work items. When we do this, the current VSTS view does not know that a work item has been updated, and does not refresh automatically.

Ideally, there would be a way to tell VSTS from an extension that a work item has been changed, but no such service is exposed at the moment. This means, users have to manually refresh the view or a specific work item after using the extension to add a new tag. For example like in this short gif (click to view full screen):

add-tag

I do think, however, that the extension still provides value, and will publish it in the marketplace soon. Let me me know in the comments if anything is unclear or doesn’t work.

Again, the code is available at https://github.com/cschleiden/vsts-extension-tags-mru. Small details might vary, but I mostly tried to keep these posts and the code in sync.

VSTS Tags MRU extension – Part 1

I often find myself adding the same tags over and over to work items. Example: While we use features to group our user stories, it is often convenient to also add a tag per feature, since these can show up on the cards on the different boards, making it easy to see what belongs to which feature:

image

So let’s say I’m working on a feature called “Tag Extension”. Our feature is broken down into a few user stories and and we have applied a tag “Tag Extension” to all of them:

image

Then we add another story using the add panel on the backlog. It’s parented to the feature but it’s missing the tag applied to the other ones:

imageimage

While I could now open the user story and add the tag, what I’d like to have is something like this:

image

Open the context menu for a work item anywhere in the product, have a list of the tags I added last to any work item, and allowing me to easily add one of them with a single click.

Fortunately, we can build this with just a few lines of code using the VSTS extensions API. There is one little drawback – more on that later – but we can get quite close to what I just described. I will be using the seed project I mentioned earlier, you can just clone the repo or download it as a zip if you want to follow along: https://github.com/cschleiden/vsts-extension-ts-seed-simple.

You can also skip immediately ahead to the finished version:
https://github.com/cschleiden/vsts-extension-tags-mru

Capturing tags as they are added

The first task to generating the MRU list is to capture which tags are added to work items. In order to receive notifications about changes to the work item form, we need to add a contribution of type ms.vss-work-web.work-item-notifications to our extension. This allows us to listen to events like onFieldChanged (a field on the form has been changed) or onSaved (work item has been saved). So, we can just replace the existing contribution in the manifest with this:

{
  "id": "tags-mru-work-item-form-observer",
  "type": "ms.vss-work-web.work-item-notifications",
  "targets": [
  "ms.vss-work-web.work-item-form"
  ],
  "properties": {
  "uri": "index.html"
  }
}

and place the matching typescript code in app.ts (replacing the existing VSS.register call):

// Register work item change listener
VSS.register("tags-mru-work-item-form-observer", (context) => {
  return {
    onFieldChanged: (args) => {
      if (args.changedFields["System.Tags"]) {
        var changedTags: string = args.changedFields["System.Tags"];

        console.log(`Tags changed: ${changedTags}`);
      }
    },
    onLoaded: (args) => {
      console.log("Work item loaded");
    },
    onUnloaded: (args) => {
      console.log("Work item unloaded");
    },
    onSaved: (args) => {
      console.log("Work item saved");
    },
    onReset: (args) => {
      console.log("Work item reset");
    },
    onRefreshed: (args) => {
      console.log("Work item refreshed");
    }
  };
});

When we publish this extension to our account, create a new work item, add a couple tags, and then save the work item, we will see messages like these in the console:

image

As you can see, all tags are reported as a single field separated by semicolons. That means, that we need a way to identify when a tag is added. An easy way to accomplish this, is to get the list of tags when a work item is opened, and then when it’s saved to diff the original and current tags.

To get the tags when the work item is opened, we can utilize the WorkItemFormService. We need to import the framework module providing it:

import TFS_Wit_Services = require("TFS/WorkItemTracking/Services");

and then we can get an instance of the service when a work item is opened, and get the current value of the System.Tags field.

onLoaded: (args) => {
  // Get original tags from work item
  TFS_Wit_Services.WorkItemFormService.getService().then(wi => {
     (<IPromise<string>>wi.getFieldValue("System.Tags")).then(
    (changedTags: string) => {
      // TODO: Save
    });
  });
}

Since it’s possible to open multiple work items in VSTS at the same time, we cannot simply store original and updated tags in two variables, but need both current and updated tags keyed to a work item, identified by its id. A simple singleton solution could be the following:

/** Split tags into string array */
function splitTags(rawTags: string): string[] {
  return rawTags.split(";").map(t => t.trim());
}

/**
 * Tags are stored as a single field, separated by ";". 
 * We need to keep track of the tags when a work item was 
 * opened, and the ones when it's closed. The intersection 
 * are the tags added.
 */
class WorkItemTagsListener {
  private static instance: WorkItemTagsListener = null;

  public static getInstance(): WorkItemTagsListener {
    if (!WorkItemTagsListener.instance) {
      WorkItemTagsListener.instance = new WorkItemTagsListener();
    }

    return WorkItemTagsListener.instance;
  }

  /** Holds tags when work item was opened */
  private orgTags: { [workItemId: number]: string[] } = {};

  /** Tags added  */
  private newTags: { [workItemId: number]: string[] } = {};

  public setOriginalTags(workItemId: number, tags: string[]) {
    this.orgTags[workItemId] = tags;
  }

  public setNewTags(workItemId: number, tags: string[]) {
    this.newTags[workItemId] = tags;
  }    

  public clearForWorkItem(workItemId: number) {
    delete this.orgTags[workItemId];
    delete this.newTags[workItemId];
  }

  public commitTagsForWorkItem(workItemId: number): IPromise {
    // Generate intersection between old and new tags
    var diffTags = this.newTags[workItemId]
      .filter(t => this.orgTags[workItemId].indexOf(t) < 0);
    // TODO: Store
    return Q(null);
  }
}

hooking it up to the observer:

 // Register work item change listener VSS.register("tags-mru-work-item-form-observer", (context) => {
  return {
    onFieldChanged: (args) => {
      // (2)
      if (args.changedFields["System.Tags"]) {
        var changedTags: string = args.changedFields["System.Tags"];
        WorkItemTagsListener.getInstance()
            .setNewTags(args.id, splitTags(changedTags));
      }
    },
    onLoaded: (args) => {
      // (1)
      // Get original tags from work item
      TFS_Wit_Services.WorkItemFormService.getService().then(wi => {
        (<IPromise>wi.getFieldValue("System.Tags")).then(
        changedTagsRaw => {
          WorkItemTagsListener.getInstance()
             .setOriginalTags(args.id, splitTags(changedTagsRaw));
        });
      });
    },
    onUnloaded: (args) => {
      // (4)
      WorkItemTagsListener.getInstance().clearForWorkItem(args.id);
    },
    onSaved: (args) => {
      // (3)
      WorkItemTagsListener.getInstance().commitTagsForWorkItem(args.id);
    },
    onReset: (args) => {
      // (5)
      WorkItemTagsListener.getInstance().setNewTags(args.id, []);
    },
    onRefreshed: (args) => {
      // (5)
      WorkItemTagsListener.getInstance().setNewTags(args.id, []);
    }
  };
});
  1. Retrieve the tags of a work item when it’s opened, storing them in the WorkItemTagsListener instance
  2. Whenever the System.Tags field is changed, store the tags as the new tags in the TagsListener instance
  3. When the work item is actually saved, commit the new tags to the MRU list (not yet implemented)
  4. Reset the work item’s data when it’s unloaded
  5. Only reset the new tags when edits to a work item are discarded

This enables us to detect added tags to any work items. The next part will cover actually storing the tags per user, showing them in a context menu, and applying to work items.

Extending VSTS – Setup

Last year Visual Studio Team Services (formerly known as Visual Studio Online) released support for extensions. There are some great samples on GitHub and a growing number of finished extensions in the marketplace. One of my published extensions is Estimate, a planning poker implementation for VSTS.

Extending VSTS is really easy, there is documentation and some great examples at the official GitHub repository.

Since I work on the Agile planning tools and work item tracking, I would like to show with a few simple examples how you can add functionality to your backlogs, boards, and queries. To make it really easy I’ve published  a small seed project that contributes a single menu item to the work item context menu and which will be the base for some extensions with a bit more functionality. If you’re already familiar with VSTS extensions feel free to skip immediately to part 2.

image

The seed project is available at GitHub; here is a step by step description how to build, publish, and install it:

  1. First you need a VSTS account, it’s free, just register with your Microsoft account.
  2. Create a publisher: With your Microsoft account, sign in at the marketplace and pick an ID and a display name:
    image
  3. Generate a personal access token: Login to VSTS, go to My Security:

    security

    then to the Personal access tokens section:

    image

    generate a token for All accessible accounts:

    image
    copy and save the generate token for later:

    image
  4. Clone (or download as zip and extract) the seed project from:
    https://github.com/cschleiden/vsts-extension-ts-seed-simple
  5. Install nodejs
  6. cd into the folder where you placed the project in step 4
  7. Install required dependencies with
    npm install
  8. Open the extension manifest, vss-extension.json
  9. Change <your-publisher> to the publisher ID you created in step 2:

    image
  10. As part of step 5, the TFS Cross Platform Command Line Interface was installed. This will be used to package and publish the extension. Login to your account by executing
    tfx login --service-url https://marketplace.visualstudio.com

    when prompted for the token, use the one generated in step 3. This will save the login information for subsequent operations:
    image(Update: the uri has changed, please use https://marketplace.visualstudio.com)

  11. Finally, execute
    grunt publish-dev
    to build, package, and publish the extension using the development configuration. If everything works the output should be similar to this:

    image
  12. Share with your account: The extension has now been published as a private extension, no one else can see it yet. To test it, we need to share it with our account. There are two ways, one is using the tfx command line, the other is using again the Marketplace. When you login again you should now see the extension and a Share button:

    image

    Just share it with your account

    image
  13. Install: After sharing the extension, it should show up in the Manage extensions page of your VSTS account:

    image
    to install, select it, confirm, and allow the requested OAuth scopes:

    imageimage

    image
  14. Test: If you now navigate to a query result (create a query and some work items if you haven’t) and open the context menu for any work item, you should see the menu action contributed by the extension:

    image
    click will execute the registered action and show the id of the selected work item:

    image

VS2013 Update 2 RC – Error APPX3210

If you stumble across the following error message with Visual Studio 2013 Update 2 RC:

1>[…]\Package.appxmanifest(19,64): error APPX3210: App manifest references the image ‘Assets/Icon150.png’ which does not have a candidate in main app package.

have a look a at the manifest file. In my case I had been using forward slashes in some of the asset paths. While this used to work before update 2 it now leads to an error. So to fix this I had to replace this:

<m2:DefaultTile Square310x310Logo="Assets\Icon310.png" 
Wide310x150Logo="Assets/Icon150.png">

with this:

<m2:DefaultTile Square310x310Logo="Assets\Icon310.png" 
Wide310x150Logo="Assets\Icon150.png">

and the error went away.

Windows 8.1 Search Indexing

Recently my Windows Search started to misbehave. It consumed large amounts of memory and CPU time. In addition, the index (in C:\ProgramData\Microsoft\Search\Data) grew to several gigabytes.

To workaround that, I tried to reduce the indexed locations (Win + W, “change how windows searches”) and rebuild the index. So I changed the locations to only index Documents and hit “Rebuild”:

image

While this fixed the issues mentioned before, something new occurred. Suddenly, every time I tried to search for something in the search charm the computer began to freeze, keystrokes were not accepted at all or with a severe delay. The troubleshooter

image

did not produce any meaningful results and rebuilding the index did not help either.

It turned out, the solution is that you have to have your AppData folder in the indexed locations. So if you have the same problem just add

C:\Users\<user>\AppData\

back the the indexed locations, rebuild your index, and you are good to go again.