TFS 2012 – Cleaning up Workflow XAML files (AKA removing versioned namespaces)

If you haven’t run into this problem in TFS Build 2012 yet, you probably will…

Problem:

After Upgrading your Visual Studio client or your build machine from 2010 to 2012, you start seeing errors like these…

  1. Opening a XAML file in Visual Studio 2012 - System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Exception: The component “Microsoft.TeamFoundation.Build.Workflow.Design.InvokeForReasonDesigner’ does not have a resource identified by the URI…
  2. Editing a build definition in Visual Studio 2012 – Unable to cast object of type ‘Microsoft.TeamFoundation.Build.Workflow.Activities.TestSpecList’ to type ‘System.Collections.Generic.List`1[Microsoft.TeamFoundation.Build.Workflow.Activities.TestSpecList]’.      
  3. Editing a build definition in Visual Studio 2012 – [A]Microsoft.TeamFoundation.Build.Workflow.Activities.BuildSettings cannot be cast to [B]Microsoft.TeamFoundation.Build.Workflow.Activities.BuildSettings. Type A originates from ‘Microsoft.TeamFoundation.Build.Workflow, Version=10.0.0.0, …’ in the context ’Default’ at location …. Type B originates from ‘Microsoft.TeamFoundation.Build.Workflow, Version=11.0.0.0, …’ in the context ‘Default’ at location ….
  4. Building on a TFS 2012 Build Machine - Validation Error: The private implementation of activity '1: DynamicActivity' has the following validation error: Compiler error(s) encountered processing expression "BuildDetail.BuildNumber". Type 'IBuildDetail' is not defined.

 

Background:

Due to a bug in one of our activities in TFS 2010, the Visual Studio Workflow Designer would add versioned namespaces to the XAML file upon saving it. They look something like this:

xmlns:mtbw1="clr-namespace:Microsoft.TeamFoundation.Build.Workflow; assembly=Microsoft.TeamFoundation.Build.Workflow, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

The version number in this string forces Workflow to attempt to load the 10.0.0.0 assemblies. These assemblies contain the same types as the 11.0.0.0 assemblies and cause all kinds of problems.

[Update: 9/27/2012]

There is another cause for some of these errors. In addition to checking your XAML, you should also look for any custom assemblies in the Controller's Custom Assembly Path that are compiled against the TFS 10.0 assemblies. Most likely, you will need to recompile any custom assemblies you have against the latest released copies of the .Net framework and the TFS assemblies.

[Update: 10/24/2012]

Fixed a bug in the code below that was removing too many of the namespaces. Some unused namespaces can still be required by the VisualBasic expressions.

Workaround:

Unfortunately, there isn’t a good way for us to fix this problem. Even if we fixed Visual Studio 2010 so that it doesn’t cause the problem, but we can’t prevent those who already have it. The best I could do is create a small program that will strip out all unused namespaces like these from the XAML. Here’s the source code…

[UPDATE: I added some code below to leave in the namespaces listed in the mc:Ignorable attribute found at the top of the file. If you have something listed there that isn't defined, you will not be able to open the XAML in the Workflow Editor.]

 using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Text.RegularExpressions;

namespace XAMLCleaner{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length == 2)
            {
                if (args[0].StartsWith("/report", StringComparison.OrdinalIgnoreCase))
                {
                    XamlCleaner.RemoveUnusedNamespaces(args[1], args[1], true);
                }
                else
                {
                    XamlCleaner.RemoveUnusedNamespaces(args[0], args[1], false);
                }
            }
            else if (args.Length == 1)
            {
                XamlCleaner.RemoveUnusedNamespaces(args[0], args[0], false);
            }
            else
            {
                PrintUsage();
            }
        }

        static void PrintUsage()
        {
            Console.WriteLine();
            Console.WriteLine("Usage:");
            Console.WriteLine(" XAMLCleaner.exe <xaml_file> [<new_xaml_file>]");
            Console.WriteLine(" - removes unused namespaces from xaml_file and");
            Console.WriteLine(" optionally puts the changes in new_xaml_file.");
            Console.WriteLine(" XAMLCleaner.exe /report <xaml_file>");
            Console.WriteLine(" - prints the unused namespaces found in xaml_file.");
            Console.WriteLine();
        }
    }

    class XmlNamespace 
    {
        public String Prefix { get; set; }
        public String Namespace { get; set; }
        public String Declaration { get; set; }
    }

    static class XamlCleaner
    {
        public static void RemoveUnusedNamespaces(String inputFile, String outputFile, bool reportOnly)
        {
            List<XmlNamespace> namespaces = new List<XmlNamespace>();
            List<String> ignoredNamespaces = new List<String>();
            String fileContents = File.ReadAllText(inputFile, Encoding.UTF8);
            String newFileContents = fileContents;
            Regex ignorableRegex = new Regex("(:Ignorable=\")(.*?)\"", RegexOptions.Singleline);
            Regex regex = new Regex("(xmlns\\:)(.*?)=\"(.*?)\"", RegexOptions.Singleline);

            foreach(Match m in ignorableRegex.Matches(fileContents))
            {
                ignoredNamespaces.AddRange(m.Groups[2].Value.Split(' '));
            }

            foreach(Match m in regex.Matches(fileContents))
            {
                namespaces.Add(new XmlNamespace() { Prefix = m.Groups[2].Value, Namespace = m.Groups[3].Value, Declaration = m.Groups[0].Value });
            }

            foreach (XmlNamespace ns in namespaces)
            {
                // Only remove namespaces that are not in the Ignore section
                // and contain version=10
                // and are not used in the file
                if (!ignoredNamespaces.Contains(ns.Prefix) && 
                    ns.Namespace.IndexOf("version=10", StringComparison.OrdinalIgnoreCase) >= 0 &&
                    !fileContents.Contains(ns.Prefix + ":")) 
                {
                    Console.WriteLine("Removing unused namespace: {0}", ns.Declaration);
                    newFileContents = newFileContents.Replace(ns.Declaration, String.Empty);
                }
            }

            if (!reportOnly)
            {
                File.WriteAllText(outputFile, newFileContents, Encoding.UTF8);
            }
             
        }
    }
}

 

Examples:

>XAMLCleaner /report DefaultTemplate.xaml

This will list all the unused namespaces, but will not actually change the file

>XAMLCleaner DefaultTemplate.xaml

This will remove all unused namespaces and overwrite the file that was read in.

>XAMLCleaner DefaultTemplate.xaml NewTemplate.xaml

This will remove all unused namespaces and write a new file called NewTemplate.xaml.

We don’t know exactly how we will end up fixing this issue since we don’t want to forcibly change all build process templates. So, for now you will have to fix these up yourself. I recommend a one time conversion of the files and then simply don’t edit them in Visual Studio 2010 any more.

Hopefully this post will save some of you some time.