0 Comments

Recently I started playing with SignalR using TypeScript, one of the things that very quickly made it's way into my project is the Hubs.tt T4 template file

Hubs.tt is a "T4 template that creates Typescript type definitions for all your Signalr hubs. If you have C# interface named "I<hubName>Client", a TS interface will be generated for the hub's client too. If you turn on XML documentation in your build, XMLDoc comments will be picked up. Licensed with http://www.apache.org/licenses/LICENSE-2.0". You can find a copy of it on GitHub using the link https://gist.github.com/htuomola/7565357. I have also placed a modified version below that updates for SignalR.Core.2.0.3.

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ output extension=".d.ts" #>
<# /* Update this line to match your version of SignalR */ #>
<#@ assembly name="$(SolutionDir)\packages\Microsoft.AspNet.SignalR.Core.2.0.3\lib\net45\Microsoft.AspNet.SignalR.Core.dll" #>
<# /* Load the current project's DLL to make sure the DefaultHubManager can find things */ #>
<#@ assembly name="$(TargetPath)" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Web" #>
<#@ assembly name="System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" #>
<#@ assembly name="System.Xml.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Threading.Tasks" #>
<#@ import namespace="Microsoft.AspNet.SignalR" #>
<#@ import namespace="Microsoft.AspNet.SignalR.Hubs" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Xml.Linq" #>
<#
var hubmanager = new DefaultHubManager(new DefaultDependencyResolver());
#>
// Get signalr.d.ts.ts from https://github.com/borisyankov/DefinitelyTyped (or delete the reference)
/// <reference path="signalr/signalr.d.ts" />
/// <reference path="jquery/jquery.d.ts" />

////////////////////
// available hubs //
////////////////////
//#region available hubs

interface SignalR {
<#
foreach (var hub in hubmanager.GetHubs())
{
#>

/**
* The hub implemented by <#=hub.HubType.FullName#>
*/
<#= FirstCharLowered(hub.Name) #> : <#= hub.HubType.Name #>;
<#
}
#>
}
//#endregion available hubs

///////////////////////
// Service Contracts //
///////////////////////
//#region service contracts
<#
foreach (var hub in hubmanager.GetHubs())
{
var hubType = hub.HubType;
string clientContractName = hubType.Namespace + ".I" + hubType.Name + "Client";
var clientType = hubType.Assembly.GetType(clientContractName);
#>

//#region <#= hub.Name#> hub

interface <#= hubType.Name #> {

/**
* This property lets you send messages to the <#= hub.Name#> hub.
*/
server : <#= hubType.Name #>Server;

/**
* The functions on this property should be replaced if you want to receive messages from the <#= hub.Name#> hub.
*/
client : <#= clientType != null?(hubType.Name+"Client"):"any"#>;
}

<#
/* Server type definition */
#>
interface <#= hubType.Name #>Server {
<#
foreach (var method in hubmanager.GetHubMethods(hub.Name ))
{
var ps = method.Parameters.Select(x => x.Name+ " : "+GetTypeContractName(x.ParameterType));
var docs = GetXmlDocForMethod(hubType.GetMethod(method.Name));

#>

/**
* Sends a "<#= FirstCharLowered(method.Name) #>" message to the <#= hub.Name#> hub.
* Contract Documentation: <#= docs.Summary #>
<#
foreach (var p in method.Parameters)
{
#>
* @param <#=p.Name#> {<#=GetTypeContractName(p.ParameterType)#>} <#=docs.ParameterSummary(p.Name)#>
<#
}
#>
* @return {JQueryPromise of <#= GetTypeContractName(method.ReturnType)#>}
*/
<#= FirstCharLowered(method.Name) #>(<#=string.Join(", ", ps)#>) : JQueryPromise<<#= GetTypeContractName(method.ReturnType)#>>;
<#
}
#>
}

<#
/* Client type definition */
#>
<#
if (clientType != null)
{
#>
interface <#= hubType.Name #>Client
{
<#
foreach (var method in clientType.GetMethods())
{
var ps = method.GetParameters().Select(x => x.Name+ " : "+GetTypeContractName(x.ParameterType));
var docs = GetXmlDocForMethod(method);

#>

/**
* Set this function with a "function(<#=string.Join(", ", ps)#>){}" to receive the "<#= FirstCharLowered(method.Name) #>" message from the <#= hub.Name#> hub.
* Contract Documentation: <#= docs.Summary #>
<#
foreach (var p in method.GetParameters())
{
#>
* @param <#=p.Name#> {<#=GetTypeContractName(p.ParameterType)#>} <#=docs.ParameterSummary(p.Name)#>
<#
}
#>
* @return {void}
*/
<#= FirstCharLowered(method.Name) #> : (<#=string.Join(", ", ps)#>) => void;
<#
}
#>
}

<#
}
#>
//#endregion <#= hub.Name#> hub

<#
}
#>
//#endregion service contracts



////////////////////
// Data Contracts //
////////////////////
//#region data contracts
<#
while(viewTypes.Count!=0)
{
var type = viewTypes.Pop();
#>


/**
* Data contract for <#= type.FullName#>
*/
interface <#= GenericSpecificName(type) #> {
<#
foreach (var property in type.GetProperties(BindingFlags.Instance|BindingFlags.Public|BindingFlags.DeclaredOnly))
{
#>
<#= property.Name#> : <#= GetTypeContractName(property.PropertyType)#>;
<#
}
#>
}
<#
}
#>

//#endregion data contracts

<#+

private Stack<Type> viewTypes = new Stack<Type>();
private HashSet<Type> doneTypes = new HashSet<Type>();

private string GetTypeContractName(Type type)
{
if (type == typeof (Task))
{
return "void /*task*/";
}

if (type.IsArray)
{
return GetTypeContractName(type.GetElementType())+"[]";
}

if (type.IsGenericType && typeof(Task<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
{
return GetTypeContractName(type.GetGenericArguments()[0]);
}

if (type.IsGenericType && typeof(Nullable<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
{
return GetTypeContractName(type.GetGenericArguments()[0]);
}

if (type.IsGenericType && typeof(List<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
{
return GetTypeContractName(type.GetGenericArguments()[0])+"[]";
}



switch (type.Name.ToLowerInvariant())
{

case "datetime":
return "string";
case "int16":
case "int32":
case "int64":
case "single":
case "double":
return "number";
case "boolean":
return "bool";
case "void":
case "string":
return type.Name.ToLowerInvariant();
}

if (!doneTypes.Contains(type))
{
doneTypes.Add(type);
viewTypes.Push(type);
}
return GenericSpecificName(type);
}

private string GenericSpecificName(Type type)
{
//todo: update for Typescript's generic syntax once invented
string name = type.Name;
int index = name.IndexOf('`');
name = index == -1 ? name : name.Substring(0, index);
if (type.IsGenericType)
{
name += "Of"+string.Join("And", type.GenericTypeArguments.Select(GenericSpecificName));
}
return name;
}

private string FirstCharLowered(string s)
{
return Regex.Replace(s, "^.", x => x.Value.ToLowerInvariant());
}

Dictionary<Assembly, XDocument> xmlDocs = new Dictionary<Assembly, XDocument>();

private XDocument XmlDocForAssembly(Assembly a)
{
XDocument value;
if (!xmlDocs.TryGetValue(a, out value))
{
var path = new Uri(a.CodeBase.Replace(".dll", ".xml")).LocalPath;
xmlDocs[a] = value = File.Exists(path) ? XDocument.Load(path) : null;
}
return value;
}

private MethodDocs GetXmlDocForMethod(MethodInfo method)
{
var xmlDocForHub = XmlDocForAssembly(method.DeclaringType.Assembly);
if (xmlDocForHub == null)
{
return new MethodDocs();
}

var methodName = string.Format("M:{0}.{1}({2})", method.DeclaringType.FullName, method.Name, string.Join(",", method.GetParameters().Select(x => x.ParameterType.FullName)));
var xElement = xmlDocForHub.Descendants("member").SingleOrDefault(x => (string) x.Attribute("name") == methodName);
return xElement==null?new MethodDocs():new MethodDocs(xElement);
}

private class MethodDocs
{
public MethodDocs()
{
Summary = "---";
Parameters = new Dictionary<string, string>();
}

public MethodDocs(XElement xElement)
{
Summary = ((string) xElement.Element("summary") ?? "").Trim();
Parameters = xElement.Elements("param").ToDictionary(x => (string) x.Attribute("name"), x=>x.Value);
}

public string Summary { get; set; }
public Dictionary<string, string> Parameters { get; set; }

public string ParameterSummary(string name)
{
if (Parameters.ContainsKey(name))
{
return Parameters[name];
}
return "";
}
}

#>

The way to use this file is to simple copy it to ~/Scripts/typings/Hubs.tt and watch the magic happen Smile. Currently I have a simple hub like below

using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SignalR_TypeScript_BasicChat.hubs
{
public class ChatHub : Hub
{
private static List<ConnectedClients> connections = new List<ConnectedClients>();

public void Connect(string displayName)
{
if (!connections.Exists(o => o.ConnectionId == Context.ConnectionId))
{
connections.Add(new ConnectedClients { ConnectionId = Context.ConnectionId, DisplayName = string.IsNullOrEmpty(displayName) ? Context.ConnectionId : displayName });
}
if (!string.IsNullOrEmpty(displayName))
{
connections.First(o => o.ConnectionId == Context.ConnectionId).DisplayName = displayName;
}
connections.First(o => o.ConnectionId == Context.ConnectionId).LastPingTime = DateTime.Now;
}

public void Disconnect()
{
if (connections.Exists(o => o.ConnectionId == Context.ConnectionId))
{
connections.Remove(connections.First(o => o.ConnectionId == Context.ConnectionId));
}
}

public ConnectedClients[] GetConnectedClients()
{
Connect(null);
return connections.Where(o => DateTime.Now.Subtract(o.LastPingTime).TotalSeconds < 15 && o.ConnectionId != Context.ConnectionId).ToArray();
}

public void SendAll(ChatMessage message)
{
Connect(message.Name);
// Call the addNewMessageToPage method to update clients.
Clients.All.addNewMessageToPage(message);
}

public void SendTo(ChatMessage message)
{
if (string.IsNullOrEmpty(message.ConnectionId) || message.ConnectionId == "everyone" || message.ConnectionId == "null")
{
SendAll(message);
}
else
{
Connect(message.Name);
// Call the addNewMessageToPage method to update clients.
Clients.Caller.addNewMessageToPage(message);
Clients.Client(message.ConnectionId).addNewMessageToPage(message);
}
}
}

public class ConnectedClients
{
public string ConnectionId { get; internal set; }
public string DisplayName { get; internal set; }
public DateTime LastPingTime { get; internal set; }
}

public interface IChatHubClient
{
void addNewMessageToPage(ChatMessage msg);
}

public class ChatMessage
{
public string Name { get; set; }
public string Message { get; set; }
public string ConnectionId { get; set; }
}
}

Having the Hubs.tt file stopped me from having to type all the code below to allow for TypeScript to build and also give me the correct schema of the hub.


// Get signalr.d.ts.ts from https://github.com/borisyankov/DefinitelyTyped (or delete the reference)
/// <reference path="signalr/signalr.d.ts" />
/// <reference path="jquery/jquery.d.ts" />

////////////////////
// available hubs //
////////////////////
//#region available hubs

interface SignalR {


/**
* The hub implemented by SignalR_TypeScript_BasicChat.hubs.ChatHub
*/
chatHub : ChatHub;

}
//#endregion available hubs

///////////////////////
// Service Contracts //
///////////////////////
//#region service contracts


//#region ChatHub hub

interface ChatHub {

/**
* This property lets you send messages to the ChatHub hub.
*/
server : ChatHubServer;

/**
* The functions on this property should be replaced if you want to receive messages from the ChatHub hub.
*/
client : ChatHubClient;
}


interface ChatHubServer {


/**
* Sends a "connect" message to the ChatHub hub.
* Contract Documentation: ---

* @param displayName {string}

* @return {JQueryPromise of void}
*/
connect(displayName : string) : JQueryPromise<void>;


/**
* Sends a "disconnect" message to the ChatHub hub.
* Contract Documentation: ---

* @return {JQueryPromise of void}
*/
disconnect() : JQueryPromise<void>;


/**
* Sends a "getConnectedClients" message to the ChatHub hub.
* Contract Documentation: ---

* @return {JQueryPromise of ConnectedClients[]}
*/
getConnectedClients() : JQueryPromise<ConnectedClients[]>;


/**
* Sends a "sendAll" message to the ChatHub hub.
* Contract Documentation: ---

* @param message {ChatMessage}

* @return {JQueryPromise of void}
*/
sendAll(message : ChatMessage) : JQueryPromise<void>;


/**
* Sends a "sendTo" message to the ChatHub hub.
* Contract Documentation: ---

* @param message {ChatMessage}

* @return {JQueryPromise of void}
*/
sendTo(message : ChatMessage) : JQueryPromise<void>;

}



interface ChatHubClient
{


/**
* Set this function with a "function(msg : ChatMessage){}" to receive the "addNewMessageToPage" message from the ChatHub hub.
* Contract Documentation: ---

* @param msg {ChatMessage}

* @return {void}
*/
addNewMessageToPage : (msg : ChatMessage) => void;

}


//#endregion ChatHub hub


//#endregion service contracts



////////////////////
// Data Contracts //
////////////////////
//#region data contracts



/**
* Data contract for SignalR_TypeScript_BasicChat.hubs.ChatMessage
*/
interface ChatMessage {

Name : string;

Message : string;

ConnectionId : string;

}



/**
* Data contract for SignalR_TypeScript_BasicChat.hubs.ConnectedClients
*/
interface ConnectedClients {

ConnectionId : string;

DisplayName : string;

LastPingTime : string;

}


//#endregion data contracts


As you can see this can be a huge time saver, especially if you changing things a lot or just want to play and not worry about the "boring" stuff like making sure you typing's match your C# code Open-mouthed smile.

0 Comments

I watch and download a lot of videos from Channel 9 and because I’m a developer and always looking for ways to speed up anything I do I searched for a PowerShell script. I eventually found one (can’t remember where) and immediately set it up to download This Week on Channel 9 and Ping Show. Over time I have added many other shows, events and series. The initial script I was found with some modifications was

$url="http://channel9.msdn.com/Shows/This+Week+On+Channel+9/feed/mp4high"
$rss=invoke-webrequest -uri $url
$destination="Z:\Media\Videos\ch9\This Week on ch9\"
[xml]$rss.Content|foreach{
  $_.SelectNodes("
rss/channel/item/enclosure")
}|foreach{
    "
Checking $($_.url.split("/")[-1]), we will skip it if it already exists in $($destination)"
  if(!(test-path ($destination + $_.url.split("
/")[-1]))){
    "
Downloading: " + $_.url
    start-bitstransfer $_.url $destination
  }
}

Because of the amount of shows growing that I was interested in I added another piece to this. On the root level of all my Channel 9 videos I added a PowerShell file that would run all the other PowerShell files that looked like

cd Z:\Media\Videos\ch9

$psScripts = Get-ChildItem -Recurse -Filter "*.ps1" | ForEach-Object -Process { if($_.Name -ne "Download All.ps1"){ Write-Text "$($_.Name)"; . $_.FullName; } }

Write-Output "Done"

This was working great as I wouldn’t need to go and run multiple files in order to update all my videos. Recently because of all the new stuff that was released because of the announcements  at #VS2013Launch I had to add about 5 new feeds to my collection. This took quite a while as I needed to create a new folder for the show, then get a copy of the first PowerShell script above and then alter the feed url and location to save the videos to. This lead to yet another script, the way I saw it I could either write a script that automates the create of the folder and script or I could do the longer but right thing and start “fresh”. The script I now run takes in an array of the url of the show, series, event or any other type of channel that has an RSS feed, basically everything after http://channel9.msdn.com/. The new and improved script looks like

cls

Write-Output "Starting"

$feedTypeNameList = $("Events/Build/2014",
"Shows/PingShow",
"Shows/This+Week+On+Channel+9",
"Series/Microsoft-Research-Luminaries",
"Shows/Windows-Azure-Friday",
"Blogs/One-Dev-Minute",
"Shows/Visual-Studio-Toolbox",
"Events/Visual-Studio/Launch-2013",
"Series/Application-Insights-for-Visual-Studio-Online",
"Series/Visual-Studio-Online",
"Series/Visual-Studio-Online-Monaco",
"Series/Visual-Studio-2012-Premium-and-Ultimate-Overview",
"Blogs/MadsKristensen",
"Blogs/C9Team",
"Series/PerfView-Tutorial",
"Blogs/IE",
"Shows/Edge")
$baseSaveLocation = "N:\s\ch9`$"

$mediaFormat = "high"
$mediaType = "mp4"
$fileExtension = "mp4"

$pathToRemoveInvalidFileNameCharsScript = "$baseSaveLocation\Remove-InvalidFileNameChars.ps1"

#------------Don't edit below here----------------#

. "$pathToRemoveInvalidFileNameCharsScript"
foreach($feedTypeName in $feedTypeNameList)
{
$channelType = $feedTypeName.Split("/")[0]
$feedUrl="http://channel9.msdn.com/$($feedTypeName.Trim("/"))/RSS/$($mediaType + $mediaFormat)"
Write-Output @"

Downloading Feed: $feedUrl

"@
$rss=invoke-webrequest -uri $feedUrl
$destination="$baseSaveLocation\$($feedTypeName.Replace("/","\").Trim("\"))\"

if (!(Test-Path $destination)) {
New-Item -ItemType directory -Path $destination
}

$videos = @()
[xml]$rss.Content|foreach{
$_.SelectNodes("rss/channel/item")|foreach{
[Array]$array = @($_.SelectSingleNode("enclosure").url,$_.SelectSingleNode("title").InnerText)
$videos += , $array
}
}
[Array]::Reverse($videos)
foreach($video in $videos){
$url = $video[0]
$title = $video[1]
if (![string]::IsNullOrEmpty("$url"))
{
$fileName = $($url.split("/")[-1])
$mp4fileName = $($fileName.Replace("." + $fileExtension,"") + "-" + (Remove-InvalidFileNameChars $title.Replace(" ","-")) + ".$fileExtension")
if ($mp4fileName.Contains("_"))
{
$pptxFileName = $mp4fileName.Remove($mp4fileName.LastIndexOf("_")) + ".pptx"
$pptxFileNameSaveAs = $mp4fileName.Remove($mp4fileName.LastIndexOf("_")) + "-" + (Remove-InvalidFileNameChars $title.Replace(" ","-")) + ".pptx"
}
else
{
$pptxFileName = $mp4fileName.Remove($mp4fileName.LastIndexOf(".")) + ".pptx"
$pptxFileNameSaveAs = $mp4fileName.Remove($mp4fileName.LastIndexOf(".")) + "-" + (Remove-InvalidFileNameChars $title.Replace(" ","-")) + ".pptx"
}

if (![string]::IsNullOrEmpty("$mp4fileName"))
{
"Checking $mp4fileName, we will skip it if it already exists in $($destination)"
#if we have the file from the previous script rename it or delete it
if(Test-Path ($destination + $fileName))
{
if(!(Test-Path ($destination + $mp4fileName)))
{
"Renaming: " + $fileName
Rename-Item $($destination + $fileName) $($destination + $mp4fileName)
}
else
{
"Deleting: " + $fileName
Remove-Item $($destination + $fileName)
}
}
else
{
#download media if it doesn'
t exists
if(!(Test-Path ($destination + $mp4fileName)))
{
$dest = $($destination + $mp4fileName)
"Downloading: '$url' to '$dest'"
Start-BitsTransfer $url $dest
}
}
}
#download pptx if it doesn't exists
if ($channelType -eq "Events") #only attempt to get pptx file for events
{
if (![string]::IsNullOrEmpty("$pptxFileName"))
{
"Checking $pptxFileName, we will skip it if it already exists in $($destination)"
$dest = $($destination + $pptxFileNameSaveAs)
if(!(Test-Path ($dest)))
{
$urlToDownload = $url.Remove($url.LastIndexOf("/")) + "/" + $pptxFileName
"Downloading: '
$urlToDownload' to '$dest'"
Start-BitsTransfer $urlToDownload $dest -ErrorAction SilentlyContinue
}
}
}
}
}
}

Write-Output "
Done"

This script caters for renaming of media files from the old just file name format to including the title in the file name. In order to add this extra script that I used for trimming funny characters from the file names. 

#source - http://gallery.technet.microsoft.com/scriptcenter/Remove-Invalid-Characters-39fa17b1

Function Remove-InvalidFileNameChars {
    <#
    .SYNOPSIS
    This is a PowerShell function to remove invalid characters from strings to be used as file names.

    .DESCRIPTION
    The function takes a string parameter called Name and returns a string that has been stripped of invalid file name characters, i.e. *, :, \, /.  The Name parameter will also receive input from the pipeline.

    .PARAMETER Name
    Specifies the file name to strip of invalid characters.

    .INPUTS
    Parameter Name accepts System.String objects from the pipeline.

    .OUTPUTS
    System.String.  Outpus a string object

    .EXAMPLE
    Remove-InvalidFileNameChars -Name "<This/name\is*an:illegal?filename>"
    PS C:\>Thisnameisanillegalfilename

    .NOTES
    It would be easiest to copy the function from the script file and place it in your profile.  However, you may also dot-source the script to load the function into PowerShell:
    i.e. PS C:\>. .\Remove-InvalidFileNameChars.ps1
    #>

    [CmdletBinding()]

    param([Parameter(Mandatory=$true,
        Position=0,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true)]
        [String]$Name
    )

    return [RegEx]::Replace($Name, "[{0}]" -f ([RegEx]::Escape([String][System.IO.Path]::GetInvalidFileNameChars())), '')
}

Hope this helps others that download Channel 9 videos to watch offline as well Smile.

0 Comments

UPDATE: This has been identified as a bug and will be fixed in the next release for on-premise

So if you have a lot customizations in your process template there is a slight chance you would have seen the error below after the upgrade.

image_thumb1

If you have completed the Configure Features option

image_thumb3

and still see the error odds are you have need to follow similar steps to below

Look for the actual exception

Take a look at the event log under the Microsoft-Team Foundation Server/Debug section of Applications and Services Log

image

When the error occurs do you see an error? I received the error below that pointed towards something with the add panel being wrong

Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.InvalidProjectSettingsException: Object reference not set to an instance of an object. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.DefaultSettingsValidatorDataProvider.GetFieldType(String workItemTypeName, String fieldReferenceName)
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProcessSettingsValidator.<>c__DisplayClass4b.<>c__DisplayClass4f.<ValidateAddPanels>b__41(String type)
   at System.Linq.Enumerable.Any[TSource](IEnumerable`1 source, Func`2 predicate)
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProcessSettingsValidator.<>c__DisplayClass4b.<ValidateAddPanels>b__40(String refName)
   at System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProcessSettingsValidator.ValidateAddPanels()
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProcessSettingsValidator.ValidateContent(OptionalFeatures featuresToValidate)
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProcessSettingsValidator.Validate(OptionalFeatures featuresToValidate, Boolean validateStructureOnly)
   --- End of inner exception stack trace ---
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProcessSettingsValidator.Validate(OptionalFeatures featuresToValidate, Boolean validateStructureOnly)
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProcessSettingsValidator.Validate(TeamFoundationRequestContext requestContext, ProjectProcessConfiguration settings, String projectUri, Boolean correctWarnings, OptionalFeatures featuresToValidate)
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProjectConfigurationService.GetProcessSettings(TeamFoundationRequestContext requestContext, String projectUri, Boolean validate, Boolean bypassCache)
   at Microsoft.TeamFoundation.Server.WebAccess.WorkItemTracking.Common.ProjectConfigurationService.GetCommonSettings(TeamFoundationRequestContext requestContext, String projectUri, Boolean validateSettings)
   at Microsoft.TeamFoundation.Server.WebAccess.Agile.AgileAreaRegistration.CanDisplayTiles(TfsWebContext webContext)

Find the problem

I went over to the process configuration which seemed to be the logical starting point based off the error on screen and stack trace from the event log. I compared all the customizations I had with what is in the default Scrum 2013.2 template and saw that I had extra fields in one of my Add Panels

image

I removed these and imported the ProcessConfiguration.xml using the witAdmin importprocessconfig tool, this worked and the site was working as normal without the error.

Fix the problem

Obviously we couldn't just remove the extra fields and be on our way because those were there for a reason. The Add Panel changes were applied for the Requirements Backlog in our case. We took a look at the Categories.xml to verify which the work item types were that were part of the requirements and found the 4 below

image

We currently only use new Product Backlog Items and the other 3 are "old" work item types that are in TFS because we used to use them and don't want to remove them because there are existing work items of this type in our TFS project. So we went to the 3 other WITd and made sure that they each had these 3 fields, we found as we expected that they didn't have each of them.

After adding the fields we re-imported the WITd for all requirement types and then tried to import the ProcessConfiguration.xml and it imported without any issues and our TFS was working again without any errors.

What Happened then?

Before the update we were obviously using this template so everything use to work, although I must admit it makes more sense that all WITd's need the fields being displayed in the Add Panel for it's categories, it use to not be the case so after upgrading this was a rather unusual error for us. It could possible have been looked at as a bug here this wasn't required to match. If I find out the reason for the change I'll update here Smile

0 Comments

Have you heard about the campaign that Packt Publishing is running at the moment? They are celebrating 2000 published titles, below are just 3 of the titles that you can get. For more info see http://bit.ly/1j26nPN.

Team Foundation Server 2013 Customization - Gordon Beeming

image

Netduino Home Automation Projects - Matt Cavanagh

image

C# 5 First Look - Joel Martinez

image

The campaign ends on the 26th March so get your books now Smile

1 Comments

If you haven't heard or read about Application Insights you should go to MSDN and read up on all the awesomeness that you can get by using this new feature of VSO. You can also take a look at the Channel 9 series on Application Insights for Visual Studio Online by Charles Sterling.

Also see Using Application Insights with a new Windows Store App for an example of adding Application Insights from the creation of a new project.

Installing Application Insights from NuGet

To add Application Insights to your project install the Application Insights Telemetry SDK for Windows Store Apps (Id: Microsoft.ApplicationInsights.Telemetry.WindowsStore)

Install through the Manage NuGet Packages

image

or using Package Manger Console

Install-Package Microsoft.ApplicationInsights.Telemetry.WindowsStore

image

The Application Insights NuGet package is now installed in your application

Getting your instrumentation key

Navigate to a dashboard on VSO and click the admin/settings button in the top right and then click on Keys & Downloads, or browse to https://AccountName.visualstudio.com/_appanalytics/_admin/keysanddownloads. Select the application you are wanting to collect usage data for

image

and then navigate down to the Windows 8.1 Store SDK section and you will see your instrumentation key

image

From here on it's the same as if you started with a clean application and opted to install Application Insights with the start of your application

Changing App Configuration

All applications (at the moment) new or current will get the error below if you haven't already changed the Platform of your store application

image_thumb[12]_thumb

To get rid of this message and put Application Insights in a state where you can start logging application usage you will need to change the configuration of your app to not be Any CPU.

Open Configuration Manager

image[29]_thumb

Set the platform to x86, x64, or ARM

image_thumb[2]

Initializing Application Insights

Open the App.xaml.cs file and at the end of the App() method add the following code

Microsoft.ApplicationInsights.Telemetry.WindowsStore.ClientAnalyticsSession.Default.Start("the instrumentation key from VSO");

At this point you can run your application and you will get some basic information like the Operating System, Screen Resolution and a view metrics like session time. You'll need to wait about 15 minutes for the information to display on your dashboard but will look something like below.

clip_image002_thumb[1]

To see the data head over to the Usage Tab on VSO Application Insights

image_thumb[7]

Logging app feature usage

My existing application happens to be an empty Split Page application so I will just log which of the groups the user clicks into. To do this (if you are using the Split Page Application) navigate to ItemsPage.xaml.cs file and change the ItemView_ItemClick method to be like below

/// <summary>
/// Invoked when an item is clicked.
/// </summary>
/// <param name="sender">The GridView (or ListView when the application is snapped)
/// displaying the item clicked.</param>
/// <param name="e">Event data that describes the item clicked.</param>
void ItemView_ItemClick(object sender, ItemClickEventArgs e)
{
var properties = new Dictionary<string, object>() {{"Title", ((SampleDataGroup)e.ClickedItem).Title}};
Microsoft.ApplicationInsights.Telemetry.WindowsStore.ClientAnalyticsChannel.Default.LogEvent("Store/ItemView", properties);

// Navigate to the appropriate destination page, configuring the new page
// by passing required information as a navigation parameter
var groupId = ((SampleDataGroup)e.ClickedItem).UniqueId;
this.Frame.Navigate(typeof(SplitPage), groupId);
}

We are simple logging that the Item Click event was fired and logging the title of the group that was clicked. This when you click around in the sample app will log to Application Insights as you click around and generate some data. To see the features that were logged head over to Event Insights under the Features tab

image_thumb[11]

Conclusion

It's really simple to get started with Application Insights and get that feedback from your application when it's out there in the wild.