TFS2010 Object Model - Alla ricerca del Work Item perduto
Scritto da
Massimo Bonanni
il
mercoledì 23 febbraio 2011
Linguaggio:
•
Framework:
•
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:

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:

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:

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:
- Qualunque siano i campi presenti nella Select WIQL, sono sempre
valorizzate le proprietà ID, Revision, AreaID, IterationID e
WorkItemType dei workitem ritornati;
- Sono valorizzati i campi che non compaiono nel punto 1 ma che
sono nella Select;
- 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;
- 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