Displaying Build Information Using Silverlight 3 and .NET RIA Services

2 08 2009

In this post we’ll look at how you can publish build information to your team as a rich internet application (RIA) using Silverlight 3 and RIA Services. The resulting application will look something like this:

image

Firstly, you need to set up your development environment by installing:

Now we can create our skeleton application:

  1. Create a new project using the Silverlight Business Application template. I’ve called my project BuildStatus.
  2. This will create two projects, one with the name you chose (e.g. BuildStatus) which is the Silverlight application and one with the name you chose followed by “.Web” which is the website that will host your Silverlight application.

image

By default the application generated by this template is configured to use Forms authentication, because this is an intranet application lets change it to use Windows authentication:

  1. Edit the Web.config in the website project and change the authentication mode from Forms to Windows.
  2. Edit the App.xaml in the Silverlight project and comment out the <appsvc:FormsAuthentication/> line and uncomment the <!–<appsvc:WindowsAuthentication/>—> line.

If you’re using Visual Basic and have Option Strict On you will need to edit LoginControl.xaml.vb and change the IIf call to use If instead to avoid the implicit cast error.

At this point run up the application and make sure it works before we go any further. I’ll wait…

In the .NET RIA Services architectural stack the Silverlight client retrieves and modifies data via a domain service. This domain service encapsulates access to the data store from the Silverlight client and transports data from the data store back to the Silverlight client.

Next we create a domain service that will return the details about the builds we want displayed. For this example we’ll show an overview of the health of the Team Project’s builds by returning the details of the latest build for each build definition.

  1. Right-click the Services folder in the website project and choose Add | New Item.
  2. Choose the Domain Service Class item template, enter the name BuildDomainService, and click Add.
  3. We’re not going to be using an existing data context or domain context so simply click OK.

We end up with an empty class that inherits from the DomainService base class. Any method we add to this class will be callable from the Silverlight client so we need to add a single function called GetLatestBuilds that returns information about the latest builds. To support client side sorting and filtering we’re going to return the list of builds as a class that implements of IQueryable(Of T).

The big question at this point is what is T? The TFS API will return BuildDetails classes that implement IBuildDetails however there are two reasons we won’t return these directly to the client:

  1. The BuildDetails class has a private constructor so we can’t transport it across tiers and .NET RIA Services don’t allow us to return interfaces from a domain service.
  2. The Silverlight client would need knowledge of the TFS API and in particular the BuildDetails class or the IBuildDetails interface.
  3. The BuildDetails class calls back to TFS to allow methods to be called on it. This would introduce a dependency between the client and TFS.
  4. We may want to change the format of certain properties or add calculated properties.

So instead, we’re going to create a data transfer object (DTO) to store the details about a build that we want to send to the client. Our DTO is going to be a plain old CLR object (POCO):

  1. Create a folder in the website project called DTOs.
  2. Add a class to the folder called BuildInfo.
  3. Add the following class definition:
Imports System.ComponentModel.DataAnnotations
Imports System.ComponentModel

Public Class BuildInfo
    Private mUri As String
    <Key()> _
    <[ReadOnly](True)> _
    Public Property Uri() As String
        Get
            Return mUri
        End Get
        Set(ByVal value As String)
            mUri = value
        End Set
    End Property

    Private mBuildDefinitionName As String
    <Display(Name:="Build Definition")> _
    <[ReadOnly](True)> _
    Public Property BuildDefinitionName() As String
        Get
            Return mBuildDefinitionName
        End Get
        Set(ByVal value As String)
            mBuildDefinitionName = value
        End Set
    End Property

    Private mBuildNumber As String
    <Display(Name:="Build Number")> _
    <[ReadOnly](True)> _
    Public Property BuildNumber() As String
        Get
            Return mBuildNumber
        End Get
        Set(ByVal value As String)
            mBuildNumber = value
        End Set
    End Property

    Private mStartTime As DateTime
    <Display(Name:="Start Time")> _
    Public Property StartTime() As DateTime
        Get
            Return mStartTime
        End Get
        Set(ByVal value As DateTime)
            mStartTime = value
        End Set
    End Property

    Private mFinishTime As DateTime
    <Display(Name:="Finish Time")> _
    <[ReadOnly](True)> _
    Public Property FinishTime() As DateTime
        Get
            Return mFinishTime
        End Get
        Set(ByVal value As DateTime)
            mFinishTime = value
        End Set
    End Property

    Private mStatus As String
    <Display(Name:="Status")> _
    <[ReadOnly](True)> _
    Public Property Status() As String
        Get
            Return mStatus
        End Get
        Set(ByVal value As String)
            mStatus = value
        End Set
    End Property
End Class

This is a very simple class definition which we’ll use to transfer information about the builds to the client. The only thing unusual you’ll notice is the use of the Display and ReadOnly attributes from the System.ComponentModel and System.ComponentModel.DataAnnotation namespaces respectively. These attributes allow us to provide additional metadata to the Silverlight client to allow us to keep more meta-data in the backend and less in the frontend.

Now it’s time to return these BuildInfo objects from our domain service. Open the BuildDomainService and add the following method:

Public Function GetLatestBuilds() As IQueryable(Of BuildInfo)
    Dim teamFoundationServerUrl As String = My.Settings.TeamFoundationServerUrl
    Dim teamProject As String = My.Settings.TeamProject

    Dim teamFoundationServer As TeamFoundationServer = TeamFoundationServerFactory.GetServer(teamFoundationServerUrl)
    Dim buildServer As IBuildServer = CType(teamFoundationServer.GetService(GetType(IBuildServer)), IBuildServer)

    Dim buildDetailSpec As IBuildDetailSpec = buildServer.CreateBuildDetailSpec(teamProject)
    buildDetailSpec.MaxBuildsPerDefinition = 1
    buildDetailSpec.InformationTypes = Nothing
    buildDetailSpec.QueryOptions = QueryOptions.Definitions
    buildDetailSpec.QueryOrder = BuildQueryOrder.FinishTimeDescending
    buildDetailSpec.Status = Microsoft.TeamFoundation.Build.Client.BuildStatus.Succeeded Or _
        Microsoft.TeamFoundation.Build.Client.BuildStatus.PartiallySucceeded Or _
        Microsoft.TeamFoundation.Build.Client.BuildStatus.Failed Or _
        Microsoft.TeamFoundation.Build.Client.BuildStatus.Stopped

    Dim queryResult As IBuildQueryResult = buildServer.QueryBuilds(buildDetailSpec)

    Dim mappedQueryResult = From b In queryResult.Builds Order By b.BuildDefinition.Name Select New BuildInfo() With { _
        .Uri = b.Uri.ToString(), _
        .BuildDefinitionName = b.BuildDefinition.Name, _
        .BuildNumber = b.BuildNumber, _
        .StartTime = b.StartTime, _
        .FinishTime = b.FinishTime, _
        .Status = b.Status.ToString() _
    }

    Return mappedQueryResult.AsQueryable()
End Function

This method retrieves the name of the Team Foundation Server and Team Project from the application’s configuration. So for this to compile (and the application to work) you’ll need to define these settings. You do this by going to the website project’s properties, clicking the Settings tab, and adding the TeamFoundationServerUrl and TeamProject settings (both with a type of String). The resulting settings should look like this:

image

At this point we have a complete backend and it’s time to turn our attention to the frontend. The first thing we need to do is add the references we need:

  1. System.Windows.Ria.Controls
  2. System.Windows.Controls.Data
  3. System.Windows.Controls.Input

Now, open up Home.xaml (which is, as you’d expect, the home page of the application and where we’ll add our grid of latest builds). Add the following namespace imports to the root Page element:

  1. xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Ria.Controls"
  2. xmlns:App="clr-namespace:MyApp.Web"
  3. xmlns:activity="clr-namespace:System.Windows.Controls;assembly=ActivityControl"
  4. xmlns:datagroup="clr-namespace:System.Windows.Data;assembly=System.Windows.Ria.Controls"
  5. xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls" 

Inside the Grid element add the following:

<ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}" >
            <StackPanel x:Name="ContentStackPanel" Style="{StaticResource ContentStackPanelStyle}">
                <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
                           Text="Latest Builds"/>

                <riaControls:DomainDataSource x:Name="dds"
                        AutoLoad="True"
                        QueryName="GetLatestBuilds"
                        LoadSize="20">

                    <riaControls:DomainDataSource.DomainContext>
                        <App:BuildDomainContext/>
                    </riaControls:DomainDataSource.DomainContext>
                </riaControls:DomainDataSource>

                <activity:Activity IsActive="{Binding IsBusy, ElementName=dds}">
                    <StackPanel>
                        <data:DataGrid x:Name="dataGrid1" Height="400" Width="800"
                                           IsReadOnly="True" AutoGenerateColumns="False"
                                           HorizontalAlignment="Left"
                                           HorizontalScrollBarVisibility="Disabled"
                                           ItemsSource="{Binding Data, ElementName=dds}">
                            <data:DataGrid.Columns>
                                <data:DataGridTextColumn Header="Build Definition" Binding="{Binding BuildDefinitionName}" />
                                <data:DataGridTextColumn Header="Build Number"  Binding="{Binding BuildNumber}" />
                                <data:DataGridTextColumn Header="Start Time"  Binding="{Binding StartTime}" />
                                <data:DataGridTextColumn Header="Finish Time"  Binding="{Binding FinishTime}" />
                                <data:DataGridTextColumn Header="Status"  Binding="{Binding Status}" />
                            </data:DataGrid.Columns>
                        </data:DataGrid>

                        <data:DataPager PageSize="20" Width="800"
                                            HorizontalAlignment="Left"
                                            Source="{Binding Data, ElementName=dds}"
                                            Margin="0,0.2,0,0" />
                    </StackPanel>
                </activity:Activity>
            </StackPanel>
        </ScrollViewer>

And we’re done! That’s right, there is NO code in the user interface. Our UI consists of:

  1. A heading.
  2. A domain data source that calls the GetLatestBuilds method on the BuildDomainService. This is configured to automatically load the data (rather than to load it on demand) and also to load the data in batches of 20. One thing to note is that the data source is referenced as App:BuildDomainContext not App:BuildDomainService as you’d expect. The BuildDomainContext is an automatically generated client class for the build domain service.
  3. An activity control that will display a “Loading” message whenever data is being retrieved by the data source.
  4. A grid and a pager to display and navigate the data returned by the data source.

You can download the source for the BuildStatus application from here.





Easily Debugging Custom Work Item Controls

17 06 2009

I’ve been doing a bit of work recently to create custom work item controls. Debugging these is tricky but you can make two configuration changes to make debugging these as easy as debugging any executable.

The first change is to configure your custom work item controls project to copy its outputs to the directory where Visual Studio Team System 2008 will look for them. To do this:

  1. Open the project properties.
  2. Switch to the Compile tab.
  3. Click Build Events.
  4. In the Post-Build Event Command Line enter:
    copy /Y "$(TargetDir)*.*" "$(UserProfile)\AppData\Local\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\9.0\"

This will copy the contents of the project’s output directory to the directory where Visual Studio Team System 2008 looks for custom controls. This uses the user’s profile directory instead of the all user’s profile because writing to the user’s profile doesn’t require administrative privileges.

The second change is to configure the project to launch Visual Studio as the process to debug. To do this:

  1. Open the project properties.
  2. Switch to the Debug tab.
  3. Select Start External Program.
  4. Browse to devenv.exe (usually C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe).

Now, when you press F5 your custom controls project will be compiled, the output copied to the directory where Visual Studio Team System looks for custom controls, and then Visual Studio Team System 2008 will be launched with the debugger attached. This will give you full debugging capability including breakpoints.





Build Automation and Complexity

8 06 2009

Siva Jagadeesan has blogged that the best approach to automate a complex manual process (and his example was a build process) is to simplify the process before trying to automate it.

This is an approach that I’ve always pushed, and to take this a step further I argue that you can’t optimise or automate a process that you haven’t defined. This is embodied in CMMI insofar as a process area cannot reach the optimizing (Level 5) maturity level without reaching the defined (Level 3) maturity level.

The process optimisation process I normally apply is:

  1. Define the manual process.
  2. Simplify/optimise the manual process.
  3. Automate the manual process.




Writing Custom Activities (aka Tasks) for Team Build 2010

2 06 2009

Aaron Hallberg has posted the first of a few posts on how to write custom activities (aka Tasks) for Team Build 2010 Beta 1, if you ever wrote custom MSBuild Tasks, or thought about it then this post is a critical read to get you up to speed.

In Team Build 2010 the orchestration of the build process is done using Workflow Foundation instead of MSBuild. For those MSBuild lovers of you out there, don’t fret, the actual building of projects is still done with MSBuild and you can invoke any MSBuild projects you want from your workflow.

http://blogs.msdn.com/aaronhallberg/archive/2009/06/01/writing-custom-activities-for-tfs-build-2010-beta-1.aspx





Test and Lab Tool Names Revealed

13 05 2009

Jason Zander has revealed the test and lab tool names:

Visual Studio® Team Test 2010
Support for the specialist tester including Web and load testing capabilities in addition to the ability to create automated test suites.  Executes in the Visual Studio environment for test professionals.  Comes with Microsoft Test and Lab Manager.

Visual Studio® Team Test 2010 Essentials
Support for the generalist tester including the ability to manage test cases and manual/automated test execution.  Installs as a scaled down product for easy access on test machines.

Visual Studio® Lab Management 2010
Support for creating virtualized environments with snapshot capabilities.  You can now execute your tests using the lab capabilities and save the state later for both development and test usage.





May is Build Month on TeamSystemLive.com

6 05 2009

Once a week during May I’ll be doing a 15 minute build-related demo via LiveMeeting on TeamSystemLive.com. Following this I and fellow MVPs Chris Tullier and D. Omar Villarreal will be hosting a 45 minute Q&A session. So come along, learn something about Team Build, and then ask all of your questions. These sessions are from 4 pm – 5 pm (GMT-6)/7 am – 8 am (GMT+10) and the first is tomorrow.

TeamSystemLive.com Chat: WiX and Team Build Together at Last

TeamSystemLive.com Chat: Running Interactive Build Agents for UI Testing and Debugging

TeamSystemLive.com Chat: Build Reusable Build Definitions

TeamSystemLive.com Chat: Introduction to Team Build 2010





Team System 2010 Feature Overview

9 04 2009

Brian Harry has posted a 10,000 foot view of the features that will be included in Team System 2010.

He will be drilling into these features in more detail in future blog posts but if you haven’t been keeping a handle on what’s going to be in Team System 2010 this is a great place to start.





Drilling Into Build Performance

19 03 2009

 

In my last post on Monitoring Build Performance I showed a report that you can use to identify when your builds aren’t performing as expected. So how do you drill into a particular build to find out where the most time was spent?

One way to do this is to run your build with diagnostic logging by passing /v:diag when you queue the build. This will increase MSBuild’s logging verbosity to diagnostic and at this level of logging when the build completes a list of each target executed and how long it was executed for will be added to the build log. For example:

      144 ms  CoreTest                                   1 calls
      467 ms  CoreDropBuild                              1 calls
      471 ms  CoreGetChangesetsAndUpdateWorkItems        1 calls
      529 ms  InitializeEndToEndIteration                1 calls
     1031 ms  CoreSimian                                 1 calls
     1295 ms  CoreInitializeWorkspace                    1 calls
     1322 ms  CoreLabel                                  1 calls
     1367 ms  Build                                      3 calls
     1888 ms  CoreCompileSolution                        1 calls
     1986 ms  CoreCompileConfiguration                   1 calls
     2179 ms  CallCompile                                1 calls
     2799 ms  CoreCompile                                3 calls
     9744 ms  CoreGet                                    1 calls

This assumes that you know before the build is executed that you want to investigate its performance, what if you want to investigate the performance of an already completed build? Or what if your log files are an unmanageable size with diagnostic logging enabled. In these circumstances we can query how long each step takes from the Team Build operational database (TfsBuild).

Caveat: The operational databases are undocumented, unsupported, you should never insert, update, or delete rows, and you should be careful querying them to minimise performance impact caused by read locks.

Still reading? Of course you are. The query below returns a list of the build steps for a particular build in descending order by duration. You’ll need to set the DefinitionName and BuildNumber to the relevant values for the build you want to query, but watch out, the values in these columns do not match exactly what you see in the user interface. The values in the database have a trailing backslash and replace some characters (for example, underscore) with a right angle bracket. You should query the build definition and build tables to check the exact values to query for.

SELECT Name,
       Message,
       CONVERT(datetime, LEFT(StartTime,23) + 'Z', 127) AS StartTime,
       CONVERT(datetime, LEFT(FinishTime,23) + 'Z', 127) AS FinishTime,
       Status,
       DATEDIFF(s,
           CONVERT(datetime, LEFT(StartTime,23) + 'Z', 127),
           CONVERT(datetime, LEFT(FinishTime,23) + 'Z', 127)
       )/60.0 AS Duration
  FROM (
           SELECT BI.NodeId,
                  BIF.FieldName,
                  BIF.FieldValue
             FROM tbl_BuildDefinition BD
                  INNER JOIN tbl_Build B ON BD.DefinitionId = B.DefinitionId
                  INNER JOIN tbl_BuildInformation BI ON B.BuildId = BI.BuildId
                  INNER JOIN tbl_BuildInformationField BIF ON BI.NodeId = BIF.NodeId
                  INNER JOIN tbl_BuildInformationType IT ON BI.NodeType = IT.NodeType
            WHERE BD.DefinitionName = 'Main>Daily\'
                  AND B.BuildNumber = '699.0.1.793\'
                  AND IT.TypeName = 'BuildStep'
                  AND BI.ParentId IS NULL
        ) SourceTable
        PIVOT
        (
            MIN(FieldValue) FOR FieldName IN ( FinishTime, Message, Name, StartTime, Status )
        ) PivotTable
ORDER BY Duration DESC

For example:

image





Monitoring Build Performance

19 03 2009

Developers love nothing more than complaining about how long their builds take, sometimes their complaints are justified and sometimes they’re not. Even worse sometimes performance degrades slowly over time and no one notices.

To better quantify how long builds are taking I created a Build Performance report that is emailed to me monthly. This report gives the minimum, maximum, and average build duration (in minutes) for each of our main build definitions over the last 12 months.

Below is an example of that report and you can download the Build Performance Report Definition for SQL Server 2005 Reporting Services from here. If you want to restrict the build definitions that are included you can add an additional condition to the WHERE clause:

AND (
    [Build].[Build Type] LIKE ‘DEV_._’
    OR [Build].[Build Type] = ‘Main’
)

image-thumb

This report was made better thanks to SSW’s Rules to Better MS SQL Reporting Services.





Error creating an instance of COM object when opening Team Explorer

17 03 2009

One of our users has been getting the error “Creating an instance of the COM component with CLSID {B69003B3-C55E-4b48-836C-BC5946FC3B28} from the IClassFactory failed due to the following error: 8007000e” whenever they open Team Explorer.

Searching the registry for this CLSID (on a different machine) identified that it was the Messenger UI Automation component used by the latest Power Tools. Because we’re not yet using the Messenger integration the solution was to simply change the Collaboration Provider to <None>. This can be done by right-clicking the Team Members node in Team Explorer, clicking Personal Settings, and clicking Change.