TFS2010 Object Model - Alla ricerca del Work Item perduto

Scritto da  Massimo Bonanni il mercoledì 23 febbraio 2011
Linguaggio: VB   •  Framework: 3.5,4.0   •  Livello: 200

Download sorgenti

Download pdf


Dopo aver visto, nei precedenti articoli, come connettersi ad un server TFS per recuperare le informazioni delle project collection e dei progetti ed aver analizzato in dettaglio come l'objecj model di TFS modella il workitem, in questo articolo vogliamo cominciare ad utilizzare i servizi che la piattaforma ci mette a disposizione e, in particolare, vogliamo vedere come sia possibile eseguire delle query per ricercare workitems.

Come sono esposti i servizi della piattaforma

Prima di entrare in dettaglio sulla parte concernente la ricerca dei work item facciamo una breve premessa su quali servizi la piattaforma TFS è in grado di erogare e, soprattutto, come facciamo a utilizzare tali servizi da codice.

La classe TfsConnection, da cui abbiamo visto, derivano le classi TfsConfigurationServer e TfsTeamProjectCollection, espone il metodo GetService (con due differenti overload) che ci consente di richiedere alla TfsConnection stessa un oggetto in grado di erogare le funzionalità per un determinato servizio.

Le due classi TfsConfigurationServer e TfsTeamProjectCollection sono, ovviamente, in grado di restituire solamente le istanze dei servizi di loro competenza e la seguente tabella mostra la distribuzione dei servizi per le due classi:

Figura1

Come possiamo vedere, ad esempio, il servizio gestione delle project collection ITeamProjectCollectionService (attraverso il quale si posssono eseguire operazioni di creazione o detatch delle collection) è recuperabile solo tramite la classe TfsConfigurationServer mentre il servizio di gestione dei workitem (WorkItemStore), di cui ci occuperemo in questo articolo, è recuperabile solo tramite la classe TfsTeamProjectCollection.

Supponiamo, quindi di avere a disposizione un'istanza della classe TfsTeamProjectCollection (la modalità con cui è possibile ottenerla è stata ampiamente dibattuta nel primo articolo della serie), per recuperare l'istanza del servizio di gestione dei work item è sufficiente scrivere:

Dim tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection("http://server:8080/tfs/collection")

Dim workItemService = CType(tpc.GetService(Of WorkItemStore)(), WorkItemStore)

 

Le Stored Query

Coloro che utilizzano il Team Explorer sono abituati ad avere a disposizione un certo numero di query memorizzate all'interno del server (o tra le personali o tra quelle del team) con cui poter eseguire svariate tipologie di ricerca: 

Figura2

Anche da codice possiamo recuperare queste query (dette StoredQuery) ed eseguirle ottenendo una collezione di workitem.

Le stored Query sono recuperabili attraverso un'istanza della classe Project (namespace Microsoft.TeamFoundation.WorkItemTracking.Client) ed in particolare utilizzando la proprietà QueryHierarchy.

Procedendo con ordine, per recuperare il progetto di cui vogliamo estrarre le stored query  possiamo utilizzare il servizio WorkItemStore della collection che contiene il progetto:

Dim tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection("http://server:8080/tfs/CollectionName")
Dim workItemService = CType(tpc.GetService(Of WorkItemStore)(), WorkItemStore)
If workItemService IsNot Nothing
         AndAlso workItemService.Projects.Contains(projectname) Then
    Dim project = workItemService.Projects("ProjectName")
    If project IsNot Nothing Then
        retlist = ExtractStoredQueriesQuery(project.QueryHierarchy)
    End If
End If

 

La proprietà Projects del servizio WorkItemStore contiene l'elenco dei progetti contenuti nella Project Collection (esattamente quella a cui il servizio è stato richiesto).

La proprietà QueryHierarchy del progetto contiene un'istanza della classe omonima che deriva dalla classe QueryFolder.

Quest'ultima  rappresenta un contenitore in grado di ospitare oggetti di tipo QueryItem (la QueryFolder deriva dalla QueryItem).

L'object model di TFS mette a disposizione due oggetti derivati da QueryItem, come mostrato nella seguente figura:

Figura3

Per trovare, quindi, tutte le query memorizzate nel nostro server  eseguiamo una ricerca, di tipo ricorsivo (visto che possiamo avere QueryFolder all'interno di QueryFolder), nella QueryHierarchy tenendo conto dei soli oggetti di tipo QueryDefinition.

Un esempio è la seguente funzione:

Private Function ExtractStoredQueries(ByVal queryFolder As QueryFolder) As IEnumerable(Of WorkItemQuery)
    Dim retList = New List(Of WorkItemQuery)
    For Each q In queryFolder
        If TypeOf q Is QueryFolder Then
            retList.AddRange(ExtractStoredQueriesQuery(CType(q, QueryFolder)))
        End If
        If TypeOf q Is QueryDefinition Then
            Dim definition = CType(q, QueryDefinition)
            Dim item = New WorkItemQuery() With {.Identifier = definition.Id,
                                          .Name = String.Format("{0} - {1}", definition.Parent.Name, definition.Name),
                                          .Description = "",
                                          .QueryText = definition.QueryText}
            retList.Add(item)
        End If
    Next
    Return retList
End Function

 

La funzione restituisce un'istanza di IEnumerable(Of  WorkItemQuery) ottenuta aggiungendo un oggetto di classe WorkItemQuery (una nostra classe che contiene le informazioni di nostro interessa delle query come nome e testo della query stessa) per ogni QueryDefinition trovata.

La classe QueryDefinition espone la proprietà QueryText che contiene la stringa della query che viene eseguita per il recupero dei WorkItems.

Il linguaggio utilizzato per descrivere le query prende il nome WIQL (acronimo di WorkItem Query Language) ed ha una sintassi molto simile a T-SQL.

Esecuzione di una query WIQL

Per capire rapidamente come funziona il WIQL, utilizziamo quanto visto in precedenza per recuperare alcune queries presenti "di serie" quando creiamo un progetto in TFS.

La seguente tabella riporta alcune delle queries standard per il template MSF for Agile:

Nome Query Query WIQL
Team Queries - All Tasks
SELECT     [System.Id], [System.WorkItemType], 
    [Microsoft.VSTS.Common.Discipline], 
    [System.State], [System.AssignedTo], 
    [Microsoft.VSTS.Common.Rank], 
    [Microsoft.VSTS.Scheduling.CompletedWork], 
    [Microsoft.VSTS.Scheduling.RemainingWork], 
    [System.Title] 
FROM    WorkItems 
WHERE   [System.TeamProject] = @project 
AND     [System.WorkItemType] = 'Task' 
AND     [System.State] = 'Active' 
ORDER BY [Microsoft.VSTS.Common.Rank], 
         [System.State], 
         [System.Id]
Team Queries - My Work Items
SELECT     [System.Id], [Microsoft.VSTS.Common.Rank], 
         [System.WorkItemType], 
    [System.State], [System.Title] 
FROM    WorkItems 
WHERE   [System.TeamProject] = @project 
AND     [System.AssignedTo] = @me 
ORDER BY [Microsoft.VSTS.Common.Rank], 
         [System.WorkItemType], 
         [System.Id]
Team Queries - Unresolved Bugs
SELECT     [System.Id], [System.WorkItemType], 
         [System.AssignedTo], [System.Reason], 
    [Microsoft.VSTS.Common.Priority], 
         [System.Title] 
FROM    WorkItems 
WHERE   [System.TeamProject] = @project 
AND     [System.WorkItemType] = 'Bug' 
AND     [System.State] = 'Resolved' 
ORDER BY [Microsoft.VSTS.Common.Priority], 
         [System.Id]

 

Analizzando la sintassi delle queries WIQL ci rendiamo conto che, di fatto, ci si trova davanti a delle Select molto simili a quelle T-SQL.
In alcuni casi sono presenti anche dei parametri (ad esempio @me o @project) all'interno delle condizioni della clausola Where.
I "campi" che possono essere recuperati in una query, sono tutti quelli presenti nelle definizioni dei Fields del WorkItem (vedere articolo precedente della serie) e, in particolare, il nome dal campo da inserire nella Select è contenuto nella proprietà ReferenceName della definizione del Field.

Supponiamo, quindi, di essere in grado di creare una query WIQL (o di utilizzare una già esistente come una StoredQuery), per eseguirla effettivamente possiamo utilizzare il metodo Query (con ben 6 overload) della classe WorkItemStore.

Un esempio di esecuzione di una query testuale scritta in WIQL per un progetto contenuto in una collection è il seguente:

Dim tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(GetCollectionUri(Me.ServerURL, collectionName))
Dim workItemService = CType(tpc.GetService(Of WorkItemStore)(), WorkItemStore)
If workItemService IsNot Nothing Then
    Dim queryArgs = New Hashtable()
    queryArgs("project") = projectname
    retList = From wi In workItemService.Query(query, queryArgs).OfType(Of Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem)()
              Select New WorkItem() With {.Identifier = wi.Id,
                                          .Name = wi.Title,
                                          .Description = wi.Description}
End If

 

A partire dall'istanza di TfsTeamProjectCollection otteniamo l'istanza di WorkItemStore sulla quale richiamiamo il metodo Query a cui passiamo il testo della query da eseguire e una struttura, che implementa IDictionary(Of String,String), che contiene i valori dei parametri.

Il metodo Query, se non è in grado di risolvere un parametro presente nella query in maniera automatica, ricerca, all'interno del dictionary ricevuto come argomento, il valore del parametro da utilizzare tramite nome del parametro stesso.

Nel codice precedente, ad esempio, se la query contiene il parametro @project, il WorkItemStore utilizza il valore da noi passato tramite l'elemento inserito nella Hashtable con la chiave "project".

Alcuni tipi di parametri possono essere risolti automaticamente dal WorkItemStore come, ad esempio, il parametro @me che viene sostituito con l'utente connesso nel caso non lo si valorizzi nel dictionary.

Possiamo, forzare la valorizzazione dei parametri risolti automaticamente semplicemente inserendo opportuni elementi nella Hashtable.

Ad esempio, se impostiamo:

 
queryArgs("me") = "Giuseppe Verdi"
 

 

sostituiamo @me con l'utente "Giuseppe Verdi" invece che con l'utente corrente.

Maggiori informazioni riguardo il linguaggio WIQL sono disponibili al seguente link.

L'esecuzione del metodo Query provoca una chiamata al server TFS che restituisce un'istanza della classe WorkItemCollection "contenente" i WorkItem che soddisfano i criteri di ricerca.

La parola "contenente" è tra virgolette perché, per ottimizzare l'accesso al server, la WorkItemCollection lavora in maniera particolare:

  1. Qualunque siano i campi presenti nella Select WIQL, sono sempre valorizzate le proprietà ID, Revision, AreaID, IterationID e WorkItemType dei workitem ritornati;
  2. Sono valorizzati i campi che non compaiono nel punto 1 ma che sono nella Select;
  3. La WorkItemCollection richiede al server TFS solo una "pagina" di workitem alla volta che, di default, è pari a 50 items (modificabile con la proprietà PageSize). Nel momento in cui, da codice, richiediamo l'accesso ad un elemento contenuto in una nuova pagina, viene eseguito un nuovo accesso al server;
  4. Se recuperiamo il valore da un campo del work item non presente nei punti 1 o 2, la WorkItemCollection esegue un accesso al server TFS per aggiornare l'intera pagina.

Da questo si deduce che è opportuno inserire nella Select tutti e soli i campi che abbiamo intenzione di utilizzare in modo da minimizzare i round-trip verso il server.

Query personalizzate

Ultimo punto da analizzare è la realizzazione di query personalizzate che, da quanto visto in precedenza, si traduce nel creare la stringa WIQL con gli opportuni parametri.

Supponiamo, come esempio molto semplice, di voler fornire ai nostri utenti la possibilità di ricercare WorkItems in base a :

  • Progetto: è possibile ricercare per uno specifico progetto o per tutti;
  • Intervallo di creazione: è possibile inserire un intervallo di date (eventualmente anche aperto) entro il quale debbono ricadere le date di creazione dei workitems;
  • Utente di assegnazione: utente a cui è assegnato il work item (può anche non essere valorizzato);
  • Project Collection: obbligatoria (abbiamo visto che il WorkItemStore è recuperabile dalla collection).

Definiamo una classe che conterrà tali filtri di ricerca:

Public Class WorkItemSearchFilters

    Public Property DateFrom As DateTime?
    Public Property DateTo As DateTime?
    Public Property CollectionName As String
    Public Property ProjectName As String
    Public Property AssignedUser As String

    Public ReadOnly Property HasFilters() As Boolean
        Get
            Dim retval As Boolean = True
            retval = retval Or DateFrom.HasValue()
            retval = retval Or DateTo.HasValue()
            retval = retval Or Not String.IsNullOrWhiteSpace(CollectionName)
            retval = retval Or Not String.IsNullOrWhiteSpace(ProjectName)
            retval = retval Or Not String.IsNullOrWhiteSpace(AssignedUser)
            Return retval
        End Get
    End Property

End Class

 

 Il codice che ci consente di eseguire la ricerca in base a tali filtri può essere del tipo: 

Dim tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(GetCollectionUri(Me.ServerURL, searchFilters.CollectionName))
Dim workItemService = CType(tpc.GetService(Of WorkItemStore)(), WorkItemStore)
Dim query = QueryHelper.GetWIQueryString(searchFilters)
retList = From wi In workItemService.Query(query).OfType(Of Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem)()
              Select New WorkItem(wi)

 

In questo caso, utilizziamo l'overload del metodo Query che prevede come argomento la sola stringa WIQL. La query in oggetto sarà creata concatenando opportunamente le stringhe grazie al metodo GetWIQueryString della classe QueryHelper:

 

Public NotInheritable Class QueryHelper

    Private Sub New()
    End Sub

    Public Shared Function GetWIQueryString(ByVal searchFilters As WorkItemSearchFilters) As String
        If searchFilters Is Nothing Then Throw New ArgumentNullException("SearchFilters")
        Dim strBuild As New StringBuilder
        With strBuild
            .Append("Select [System.Id], [System.WorkItemType], [Microsoft.VSTS.Common.Discipline], [System.State], [System.AssignedTo], [Microsoft.VSTS.Common.Rank], [Microsoft.VSTS.Scheduling.CompletedWork], [Microsoft.VSTS.Scheduling.RemainingWork], [System.Title] FROM WorkItems ")
            If searchFilters.HasFilters Then
                Dim appendAnd As Boolean = False
                .Append("Where ")
                If searchFilters.DateFrom.HasValue Then
                    If appendAnd Then
                        .Append("AND ")
                    End If
                    appendAnd = True
                    .AppendFormat("[System.CreatedDate] >= '{0:dd/MM/yyyy}' ", searchFilters.DateFrom.Value)
                End If
                If searchFilters.DateTo.HasValue Then
                    If appendAnd Then
                        .Append("AND ")
                    End If
                    appendAnd = True
                    .AppendFormat("[System.CreatedDate] <= '{0:dd/MM/yyyy}' ", searchFilters.DateTo.Value)
                End If
                If Not String.IsNullOrWhiteSpace(searchFilters.AssignedUser) Then
                    If appendAnd Then
                        .Append("AND ")
                    End If
                    appendAnd = True
                    .AppendFormat("[System.AssignedTo] = '{0}' ", searchFilters.AssignedUser)
                End If
                If Not String.IsNullOrWhiteSpace(searchFilters.ProjectName) Then
                    If appendAnd Then
                        .Append("AND ")
                    End If
                    appendAnd = True
                    .AppendFormat("[System.TeamProject] = '{0}' ", searchFilters.ProjectName)
                End If
            End If
            .Append("Order By [System.Id]")
        End With
        Return strBuild.ToString()
    End Function
End Class

 

 Se ad esempio volessimo cercare tutti i workitems dell'utente "Giuseppe Verdi" tra il primo gennaio 2010 e il 31 dicembre 2010, potremmo scrivere:

Dim filters = New WorkItemSearchFilters
With filters
    .DateFrom = New DateTime(2010, 1, 1)
    .DateTo = New DateTime(2010, 12, 31)
    .AssignedUser = "Giuseppe Verdi"
End With

Dim query = QueryHelper.GetWIQueryString(filters)

 

ed otterremmo:

SELECT  [System.Id], [System.WorkItemType],
        [Microsoft.VSTS.Common.Discipline], [System.State], [System.AssignedTo],
        [Microsoft.VSTS.Common.Rank], [Microsoft.VSTS.Scheduling.CompletedWork],
        [Microsoft.VSTS.Scheduling.RemainingWork], [System.Title]
FROM    WorkItems
WHERE   [System.CreatedDate] >= '01/01/2010'
AND     [System.CreatedDate] <= '31/12/2010'
AND     [System.AssignedTo] = 'Giuseppe Verdi'
ORDER BY     [System.Id]

 

 Un modo rapido per conoscere il nome corretto dei campi da mettere nella Select o nella Where è quello di aprire la tabella Fields di una qualsiasi collection in cui è presente il campo ReferenceName che fornisce esattamente ciò che cerchiamo.

 

Riferimenti

[1] Team Foundation Server Architecture: http://msdn.microsoft.com/en-us/library/ms252473.aspx
[2] Extending Team Foundation: http://msdn.microsoft.com/en-us/library/bb130146.aspx
[3] Team Foundation Server 2010 SDK: http://code.msdn.microsoft.com/TfsSdk
[4] Team Foundation Server Team Blog: http://blogs.msdn.com/b/team_foundation/


Tags: TFS,Team Foundation,Team Foundation Server

 
x