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:
Firstly, you need to set up your development environment by installing:
Now we can create our skeleton application:
- Create a new project using the Silverlight Business Application template. I’ve called my project BuildStatus.
- 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.
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:
- Edit the Web.config in the website project and change the authentication mode from Forms to Windows.
- 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.
- Right-click the Services folder in the website project and choose Add | New Item.
- Choose the Domain Service Class item template, enter the name BuildDomainService, and click Add.
- 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:
- 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.
- The Silverlight client would need knowledge of the TFS API and in particular the BuildDetails class or the IBuildDetails interface.
- 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.
- 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):
- Create a folder in the website project called DTOs.
- Add a class to the folder called BuildInfo.
- 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:
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:
- System.Windows.Ria.Controls
- System.Windows.Controls.Data
- 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:
- xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Ria.Controls"
- xmlns:App="clr-namespace:MyApp.Web"
- xmlns:activity="clr-namespace:System.Windows.Controls;assembly=ActivityControl"
- xmlns:datagroup="clr-namespace:System.Windows.Data;assembly=System.Windows.Ria.Controls"
- 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:
- A heading.
- 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.
- An activity control that will display a “Loading” message whenever data is being retrieved by the data source.
- 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.
