How to: Implement, package and deploy custom check-in policy for TFS 2010

This How to article walks you through the process of creating, packaging, deploying and applying a custom check-in policy for TFS. Check-in policies let you run rules, whenever someone attempts to check-in a set of changes, to ensure that the changes meet a specific set of criteria. As an example, this article illustrates how to ensure that C# and VB.NET source files comply with formatting guidelines using the excellent .NET code organizer, formatter and beautifier, NArrange. To implement your own custom check-in policy, you create a class that derives from PolicyBase and implements the IPolicyDefinition and IPolicyEvaluation interfaces. Once the assembly containing this class must be registered appropriately in the Microsoft Windows® registry, you can apply the policy to your team project.

Before You Begin

To complete this walkthrough, you’ll need the Edit project-level information permission for the Team Project you want to apply the check-in policy to. For more information, see Team Foundation Permissions.

Step 1 – Create and Build a Custom Policy Class

In this initial step, you create a new class library and add a custom policy class by deriving from the PolicyBase base class in the Microsoft.TeamFoundation.VersionControl.Client namespace. By deriving from this base class, your class implements the IPolicyDefinition and IPolicyEvaluation interfaces. The example policy code below applies code formatting standards to each C# and VB.NET source file that's checked in using NArrange.

 namespace TeamFoundation.Samples.CheckinPolicies
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    using Microsoft.TeamFoundation.VersionControl.Client;

    using NArrange.Core;

    [Serializable]
    public class ArrangePolicy : PolicyBase, ILogger
    {
        #region Fields

        [NonSerialized]
        IPendingCheckin _pendingCheckin;

        #endregion Fields

        #region Properties

        /// <summary>
        /// Gets a value indicating whether this check-in policy has an editable configuration
        /// </summary>
        /// <value><c>true</c> if this instance can edit; otherwise, <c>false</c>.</value>
        public override bool CanEdit
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Gets the description.
        /// </summary>
        /// <value>The description.</value>
        public override string Description
        {
            get { return "Runs NArrange against any .cs and .vb files in your pending changes"; }
        }

        /// <summary>
        /// Gets the type.
        /// </summary>
        /// <value>The type.</value>
        public override string Type
        {
            get { return "NArrange Policy"; }
        }

        /// <summary>
        /// Gets the type description.
        /// </summary>
        /// <value>The type description.</value>
        public override string TypeDescription
        {
            get { return "This policy prevents users from checking in files that don't comply with your coding style guidelines."; }
        }

        #endregion Properties

        #region Methods

        /// <summary>
        /// Dispose method unsubscribes to the event so we don't get into 
        /// scenarios that can create null ref exceptions
        /// </summary>
        public override void Dispose()
        {
            base.Dispose();
            _pendingCheckin.PendingChanges.CheckedPendingChangesChanged -= PendingChanges_CheckedPendingChangesChanged;
        }

        public override bool Edit(IPolicyEditArgs policyEditArgs)
        {
            // no policy settings to save
            return true;
        }

        /// <summary>
        /// Evaluates the pending changes for policy violations
        /// </summary>
        /// <returns></returns>
        public override PolicyFailure[] Evaluate()
        {
            var failures = new List<policyfailure>();
            const String supportedSourceFileExtensions = ".cs;.vb";

            // get a temporary location to write out the configuration file for NArrange
            var configFilePath = Path.GetTempFileName();

            // get the configuration file from our resources and write it to disk
            using (var writer = new StreamWriter(configFilePath))
            {
                writer.Write(Resources.DefaultConfig);
            }

            // instantiate a new FileArranger - this is the engine for NArrange
            var fileArranger = new FileArranger(configFilePath, this);

            // process each file in the set of pending changes
            foreach (var pendingChange in PendingCheckin.PendingChanges.CheckedPendingChanges)
            {
                // invoke NArrange on C# and VB.NET source files only
                if (supportedSourceFileExtensions.Contains(Path.GetExtension(
                    pendingChange.LocalItem).ToLowerInvariant()))
                {
                    fileArranger.Arrange(pendingChange.LocalItem, pendingChange.LocalItem, true);
                }
            }

            // clean up the temporary copy of the configuration file
            File.Delete(configFilePath);

            return failures.ToArray();
        }

        /// <summary>
        /// Initializes this policy for the specified pending checkin.
        /// </summary>
        /// <param name="pendingCheckin" />The pending checkin.</param>
        public override void Initialize(IPendingCheckin pendingCheckin)
        {
            base.Initialize(pendingCheckin);

            _pendingCheckin = pendingCheckin;
            pendingCheckin.PendingChanges.CheckedPendingChangesChanged += PendingChanges_CheckedPendingChangesChanged;
        }

        /// <summary>
        /// Logs a message.
        /// </summary>
        /// <param name="level" />Log level.</param>
        /// <param name="message" />Log message.</param>
        /// <param name="args" />Message arguments.</param>
        public void LogMessage(LogLevel level, string message, params object[] args)
        {
        }

        /// <summary>
        /// Subscribes to the PendingChanges_CheckedPendingChangesChanged event 
        /// and reevaluates the policy. You should not do this if your policy takes 
        /// a long time to evaluate since this will get fired every time there is a
        /// change to one of the items in the pending changes window.
        /// </summary>
        /// <param name="sender" />The source of the event.</param>
        /// <param name="e" />The <see cref="System.EventArgs" /> instance containing the event data.</param>
        void PendingChanges_CheckedPendingChangesChanged(object sender, EventArgs e)
        {
            if (!Disposed)
            {
                OnPolicyStateChanged(Evaluate());
            }
        }

        #endregion Methods
    }
}

Note that, for this check-in policy, I’ve added a copy of NArrange’s default configuration file (DefaultConfig.xml) as a file resource which I write out to disk when needed to pass to the FileArranger object.

Step 2 – Package the Custom Check-in Policy

The easiest way to package your check-in policy for deployment is to use VSIX Deployment. A VSIX package is a ZIP file that follows the Open Packaging Conventions (OPC) standard. The package contains binaries and supporting files, together with a [Content_Types].xml file and a .vsix manifest file. One VSIX package may contain the output of multiple projects, or even multiple packages that have their own manifests. For more information about the contents of a VSIX package, see Anatomy of a VSIX Package.

For this check-in policy, we’ll add a new VSIX Project to our solution as shown below:

Add New VSIX Project

To get the required registry key created, we’ll need to add a pkgdef file to our VSIX package the tells it where to create the key and what value to set as shown below:

 [$RootKey$\TeamFoundation\SourceControl\Checkin Policies]
"TeamFoundation.Samples.CheckinPolicies"="$PackageFolder$\TeamFoundation.Samples.CheckinPolicies.dll"

This creates a key for our assembly named after our namespace which will allow Team Explorer to locate our check-in policy. Note that everyone who will be checking in files affected by your check-in policy will need to install it on their machine(s). Luckily, installing VSIX packages is really easy. You just double click on the VSIX file (generated at build time by the VSIX Project) and you’ll see a dialog box like this:

image

You can edit the .vsixmanifest file in your VSIX Project to customize attributes of your package like the name, description, license, icon, preview image, etc.

Step 3 – Apply the Custom Policy

In this step you add the custom policy to your team project. This ensures that the policy is evaluated each time a developer checks in a file to this team project.

  1. In Team Explorer, right-click your team project, point to Team Project Settings and then click Source Control.
  2. Click the Check-in Policy tab and then click Add.
  3. Select the NArrange Policy and click OK and then OK again.

The policy is now applied each time a developer checks a file into this team project.

Step 4 – Validate the Custom Policy

In this step, you check-in a source file to ensure that the custom policy works correctly.

  1. Make a change to a C# or VB.NET source file and then check-in the file.
  2. Verify that the source file is organized, formatted and beautified by NArrange.

Additional Resources

You can download the sources for this check-in policy from Code GalleryCheckinPolicies.zip