Archive

Archive for the ‘Team Foundation Server’ Category

Using the Team Foundation Service OData API from a Windows Store app – Part two – Changesets

09/03/2013 Comments off

In the first article of this series, we have talked about how to connect to Team Foundation Service using the new OData API to retrieve our team projects. Now we’ll see how to get the list of changesets of a certain project.

First of all, we need to update our model classes to store the new information. Let’s start from what we have created in the previous post (if you haven’t done it yet, you can download the app we have realized). Create a new class with name Changeset:

public class Changeset
{
    public string Name { get; set; }
    public string Description { get; set; }
    public Uri Uri { get; set; }
    public DateTimeOffset Date { get; set; }
    public string Author { get; set; }
}

Then, update the Project class with a property that holds the Uri of the resource that we must query to retrieve the Changesets list:

public class Project
{
    public string Name { get; set; }
    public Uri ChangesetsUri { get; set; }
}

Finally, we need to make a change to the TfsConnector class too. In particular, we need to define an overload for the GetAsync method that takes an Uri as input parameter. It represents the complete Url of the request, while the old method version, that takes a string, mantains the old behavior:

public class TfsConnector
{
    // ...
    
    private async Task<SyndicationFeed> GetAsync(string path)
    {
        var uri = new Uri(ServiceEndpoint + path);
        return await this.GetAsync(uri);
    }

    private async Task<SyndicationFeed> GetAsync(Uri uri)
    {
        var client = this.GetSyndicationClient();
        var feed = await client.RetrieveFeedAsync(uri);

        return feed;
    }
}

We need this new version because in the Project class we’ll save the complete Url for Changesets request, as it comes from the Project XML feed. Let’t see how to do that.

As we have seen in the first article, The XML feed that is returned when we make a request on the /Projects path contains, for each project, a list of link tags, each of one represeting the Url of a resource that can queried to obtain the corresponding information. For example:

<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Changesets" 
      type="application/atom+xml;type=feed" 
      title="Changesets" 
      href="Projects('MyProject')/Changesets" /> 
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Builds" 
      type="application/atom+xml;type=feed" 
      title="Builds" 
      href="Projects('MyProject')/Builds" /> 
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/BuildDefinitions" 
      type="application/atom+xml;type=feed" 
      title="BuildDefinitions" 
      href="Projects('MyProject')/BuildDefinitions" /> 

So, if we want to retrieve the Url for Changesets requests, we can search for a link tag whose title is equal to Changesets. We can do that in the GetProjectsAsync method of TfsConnector, that becomes as follows:

public async Task<IEnumerable<Project>> GetProjectsAsync()
{
    var projects = new List<Project>();
    var feed = await this.GetAsync(PROJECTS_PATH);

    foreach (SyndicationItem item in feed.Items)
    {
        var project = new Project
        {
            Name = item.Title.Text,
            ChangesetsUri = item.Links.Where(
                            l => l.Title.ToLower() == "changesets")
                            .Select(l => l.Uri).FirstOrDefault()
        };
        projects.Add(project);
    }

    return projects.OrderBy(p => p.Name);
}

Next, before analyzing the actual method that returns the Changesets list, take a look to another helper method:

private string GetTagElement(XmlDocument content, string tagName)
{
    var element = content.GetElementsByTagName("d:" + tagName);
    if (element != null)
        return ((XmlElement)(element.Item(0))).InnerText;

    return null;
}

Because some information from the feed is contained in tags that belong to a custom namespace, with the GetTagElement method we can easily retrive this data without having to worry about the namespace to use. As we’ll see in the next articles, we’ll use it many, times even for other type of requests. So, if you prefer, you can make it an extension method for the XmlDocument type.

Now, we have all the pieces that we need to actually retrieve the changesets list for a certain project. Add the following method to the TfsConnector class:

public async Task<IEnumerable<Changeset>> GetChangesetsAsync(Uri changesetsUri)
{
    var changesets = new List<Changeset>();
    var feed = await this.GetAsync(changesetsUri);

    foreach (SyndicationItem item in feed.Items)
    {
        var changeset = new Changeset
        {
            Name = item.Links.Where(
                    l => l.Title.ToLower() == "changeset")
                    .Select(l => l.Uri)
                    .FirstOrDefault().ToString().Split('/').Last(),
            Description = item.Summary.Text,
            Date = item.LastUpdatedTime,
            Uri = new Uri(this.GetTagElement(item.Content.Xml, "WebEditorUrl")),
            Author = this.GetTagElement(item.Content.Xml, "Committer"),
        };

        changesets.Add(changeset);
    }

    return changesets;
}

This code extract information from an XML element like to following:

<entry m:etag="W/"datetime'2012-12-07T22%3A55%3A15.337%2B00%3A00'"">
  <id>https://tfsodata.visualstudio.com/DefaultCollection/Changesets(113)</id> 
  <title type="text">vstfs:///VersionControl/Changeset/113</title> 
  <summary type="text">Changeset description</summary> 
  <updated>2012-12-07T22:55:15Z</updated> 
  <author>
    <name /> 
  </author>
  <link rel="edit" title="Changeset" href="Changesets(113)" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Changes"
        type="application/atom+xml;type=feed" 
        title="Changes" href="Changesets(113)/Changes" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/WorkItems" 
        type="application/atom+xml;type=feed"
        title="WorkItems" href="Changesets(113)/WorkItems" /> 
  <category term="Microsoft.Samples.DPE.ODataTFS.Model.Entities.Changeset" 
        scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> 
  <content type="application/xml">
    <m:properties>
      <d:Id m:type="Edm.Int32">113</d:Id> 
      <d:ArtifactUri>vstfs:///VersionControl/Changeset/113</d:ArtifactUri> 
      <d:Comment>Changeset description</d:Comment> 
      <d:Committer>Windows Live ID\commiter_mail@mail.com</d:Committer> 
      <d:CreationDate m:type="Edm.DateTime">2012-12-07T22:55:15.337+00:00</d:CreationDate> 
      <d:Owner>Windows Live ID\owner_mail@mail.com</d:Owner> 
      <d:Branch m:null="true" /> 
      <d:WebEditorUrl>
        https://mydomain.visualstudio.com/web/cs.aspx?pcguid=95c4fe5e-057c-45ee-ab78-5e689ba82238&cs=113
      </d:WebEditorUrl> 
    </m:properties>
  </content>
</entry>

Finally, we can update our Windows Store app. Remember that in the MainPage.xaml we have defined a layout with two columns. We’ll use the second one to show the changesets of the selected project. So, Add the following code to XAML declarations:

<Border Grid.Column="1" BorderThickness="1" BorderBrush="LightGray" Margin="10,0,0,0">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <TextBlock x:Name="SelectedProjectTextBlock" Margin="10"                               
                   Style="{StaticResource SubheaderTextStyle}"></TextBlock>
        <GridView Grid.Row="1"
            x:Name="detailsGridView"
            SelectionMode="None"
            IsItemClickEnabled="True"
            ItemClick="detailsGridView_ItemClick">
            <GridView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Height="140" Width="480" Margin="10">
                        <TextBlock Text="{Binding Name}" 
                                Style="{StaticResource TitleTextStyle}" Margin="5,5,0,0"/>
                        <TextBlock Text="{Binding Date}"
                                Style="{StaticResource ItemTextStyle}" Margin="5,5,0,0"/>
                        <TextBlock Text="{Binding Author}"
                                Style="{StaticResource ItemTextStyle}" Margin="5,5,0,0"/>
                        <TextBlock Text="{Binding Description}"
                                Style="{StaticResource BodyTextStyle}" Margin="5,5,0,0"
                                TextWrapping="Wrap" TextTrimming="WordEllipsis" />
                    </StackPanel>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>
    </Grid>
</Border>

We also need to update the definition of Project ListView in order to add the handler of the ItemClick event:

<ListView x:Name="itemsListView"
          SelectionMode="None"
          IsItemClickEnabled="True"
          ItemClick="itemsListView_ItemClick">

It’s the turn of the code in the MainPage.xaml.cs file. First, define the method that handles the itemsListView_ItemClick event:

private async void itemsListView_ItemClick(object sender, ItemClickEventArgs e)
{
    var project = e.ClickedItem as Project;
    SelectedProjectTextBlock.Text = "Changesets of " + project.Name;
    var changesets = await tfsConnector.GetChangesetsAsync(project.ChangesetsUri);
    detailsGridView.ItemsSource = changesets;
}

We get the clicked project and then we call the GetChangesetsAsync method on TfsConnector, using the Uri that we have retrieved when we obtained the project list.

Finally, we need to define also the handler for the detailsGridView_ItemClick method. When the user clicks a changeset, we want to open its Web page in the browser:

private async void detailsGridView_ItemClick(object sender, ItemClickEventArgs e)
{
    var changeset = e.ClickedItem as Changeset;
    await Windows.System.Launcher.LaunchUriAsync(changeset.Uri);
}

It’s all. Now we can start then app and tap a project to see its changesets:

TFS Changesets List

TFS Changesets List

This completes the second article about Team Foundation Service OData API in Windows Store apps. As usual, the app is available for download:

TfsOData_Part_Two

Next time we will take about builds.

Advertisements

Using the Team Foundation Service OData API from a Windows Store app – Part one

05/03/2013 1 comment

With the recent introduction of Team Foundation Service OData API, we can make OData queries against Team Foundation Service to obtain the list of our projects, changesets, builds, work items, and so on. In this series of posts, I’ll show how to build a client for Windows Store that is able to consume this data.

The first thing to do is is to enable and configure basic authentication credentials, as you can read in the section Team Foundation Service authentication at https://tfsodata.visualstudio.com:

Setting TFS alternate Credentials

Setting TFS alternate Credentials

Now, we can move on the Windows Store app. In this first post, we’ll see how to connect to Team Foundation Service and get the list of team projects.

Create a new project using the Blank App (XAML). Then, delete the MainPage.xaml file, right click on the project in the Solution Explorer and select the Add | New Item… command. Now, choose Basic Page as template and name it MainPage. In this way, we have a basic structure on which we’ll add all the functionalities of our client. Add the following XAML objects to the page:

<Grid Grid.Row="1" Margin="116,0,40,46">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"></ColumnDefinition>
        <ColumnDefinition Width="*"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <ListView x:Name="itemsListView"
              SelectionMode="None">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}" 
                        Foreground=
                        "{StaticResource ListViewItemOverlayForegroundThemeBrush}" 
                        Style="{StaticResource TitleTextStyle}" Height="60" 
                        TextWrapping="Wrap"
                        Margin="15,5,15,0"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

    <Border Grid.Column="1" BorderThickness="1" BorderBrush="LightGray" Margin="10,0,0,0">

    </Border>
</Grid>

We have defined a Grid with two columns: on the left, there is ListView that will show all our projects, while we’ll use the right column to show information about the selected project (changesets, builds, and so on). For this first article, it will have no contents.

As default, the OData API returns XML feeds, so we can use the SyndicationClient object to retrieve them (alternatively, we have the possibility to get JSON). Create a Project class:

public class Project
{
    public string Name { get; set; }
}

At this moment, it contains only the name of the project, but we’ll add new properties in the next articles. Now, define a TfsConnector class, that handles all the communications with Team Foundation Service:

public class TfsConnector
{
    public string Domain { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public string ServiceEndpoint { get; set; }

    private string authorizationHeader;

    private const string PROJECTS_PATH = "/Projects";

    public async Task<IEnumerable<Project>> GetProjectsAsync()
    {
        var projects = new List<Project>();
        var feed = await this.GetAsync(PROJECTS_PATH);

        foreach (SyndicationItem item in feed.Items)
        {
            var project = new Project { Name = item.Title.Text };
            projects.Add(project);
        }

        return projects.OrderBy(p => p.Name);
    }

    private async Task<SyndicationFeed> GetAsync(string path)
    {
        var client = this.GetSyndicationClient();
        var uri = new Uri(ServiceEndpoint + path);
        var feed = await client.RetrieveFeedAsync(uri);

        return feed;
    }

    private string GetAuthorizationHeader()
    {
        if (string.IsNullOrEmpty(authorizationHeader))
        {
            var credentials = string.Format(CultureInfo.InvariantCulture, @"{0}\{1}:{2}",
                                     Domain, UserName, Password);
            authorizationHeader = "Basic " +
                Convert.ToBase64String(
                Encoding.GetEncoding("us-ascii").GetBytes(credentials));
        }

        return authorizationHeader;
    }

    private SyndicationClient GetSyndicationClient()
    {
        var syndicationClient = new SyndicationClient();
        syndicationClient.Timeout = UInt32.MaxValue;
        syndicationClient.BypassCacheOnRetrieve = true;
        syndicationClient.SetRequestHeader("Authorization", 
            this.GetAuthorizationHeader());

        return syndicationClient;
    }
}

In order to connect to Team Foundation Service and use OData, we need four parameters:

  1. Domain is the domain of our account (for example, in http://myaccount.visualstudio.com, the Domain is myaccount);
  2. UserName is the user name you use to log in to Team Foundation Service. If you haven’t set a secondary user name in the Credentials tab of the User Profile window, it is your Microsoft Account user name;
  3. Password is the alternate password you have set in the Credentials tab;
  4. ServiceEndpoint is the endpoint of the OData service. For Team Foundation Service, it is https://tfsodata.visualstudio.com/DefaultCollection.

The first three properties are used to create the basic authorization header, as you can see in the GetAuthorizationHeader method. The latter is invoked in the GetSyndicationClient method, that instantiates the object used to retrieve OData response. This is, in turn, called by the GetAsync method, that takes a path as parameter, constructs the resource Uri using the ServiceEndpoint and then retrieves the feed with the requested information.

All these methods are needed by the GetProjectsAsync method, that makes a request on the /Projects path and returns a list with all the team projects. It extract information from an XML element like the following:

<entry>
  <id>https://tfsodata.visualstudio.com/DefaultCollection/Projects('MyProject')</id> 
  <title type="text">MyProject</title> 
  <updated>2013-03-09T16:07:04Z</updated> 
  <author>
    <name /> 
  </author>
  <link rel="edit" title="Project" href="Projects('MyProject')" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Changesets" 
        type="application/atom+xml;type=feed"
        title="Changesets" href="Projects('MyProject')/Changesets" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Builds" 
        type="application/atom+xml;type=feed" 
        title="Builds" href="Projects('MyProject')/Builds" /> 
  <link 
      rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/BuildDefinitions" 
      type="application/atom+xml;type=feed" 
      title="BuildDefinitions" href="Projects('MyProject')/BuildDefinitions" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/WorkItems" 
      type="application/atom+xml;type=feed" 
      title="WorkItems" href="Projects('MyProject')/WorkItems" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Queries" 
      type="application/atom+xml;type=feed" 
      title="Queries" href="Projects('MyProject')/Queries" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Branches" 
      type="application/atom+xml;type=feed" 
      title="Branches" href="Projects('MyProject')/Branches" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/AreaPaths" 
      type="application/atom+xml;type=feed" 
      title="AreaPaths" href="Projects('MyProject')/AreaPaths" /> 
  <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/IterationPaths" 
      type="application/atom+xml;type=feed" 
      title="IterationPaths" href="Projects('MyProject')/IterationPaths" /> 
  <category term="Microsoft.Samples.DPE.ODataTFS.Model.Entities.Project" 
      scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> 
  <content type="application/xml">
    <m:properties>
      <d:Name>MyProject</d:Name> 
      <d:Collection>https://mydomain.visualstudio.com/DefaultCollection</d:Collection> 
    </m:properties>
  </content>
</entry>

Now, we can call the TfsConnector class in the LoadState method of MainPage.xaml.cs:

protected override async void LoadState(Object navigationParameter, 
    Dictionary<String, Object> pageState)
{
    var tfsConnector = new TfsConnector
    {
        Domain = "my_tfs_domain",
        UserName = "my_tfs_username",
        Password = "my_tfs_password",
        ServiceEndpoint = "https://tfsodata.visualstudio.com/DefaultCollection"
    };

    var projects = await tfsConnector.GetProjectsAsync();
    itemsListView.ItemsSource = projects;
}

Our demo is ready. We can execute the project and, after some seconds, we’ll see all the team projects in the list:

TFS Projects List

TFS Projects List

The app we have described in this post is avaible for download:

TfsOData_PartOne

Note that the service is in beta, so the API is subject to change in any moment and there is room for performance improvement.

In the next article, we’ll talk about how to retrieve changesets.