10 gennaio 2009

Articolo : Un reminder per Team Foundation Server 2005

In questo post vorrei pubblicare un mio vecchio articolo (di un annetto fa) che mostra come interrogare l’object model di team Foundation Server (in questo caso nella versione 2005) per ottenere le attività da svolgere in riferimento ad un utente.

Quest’articolo prende spunto dalla realizzazione di un semplice software che avverte l’utente della presenza di nuovi elementi di lavoro assegnatigli in Team Foundation Server, per esaminare le potenzialità del tool di gestione dei progetti di Microsoft a proposito della sua estensibilità ed espandibilità. In particolare prenderemo in esame il sottoinsieme di API riguardanti la gestione dei work items.

L’object model di Team Foundation Server

Prima di cominciare a progettare il nostro software, occupiamoci di dare un’occhiata all’object model di Team Foundation Server, cioè all’insieme di classi messe a disposizione degli sviluppatori per interagire con il server di Team Foundation.

Innanzitutto osserviamo che Team Foundation utilizza SQL Server 2005 quale base dati per memorizzare tutto ciò che riguarda i progetti da lui gestiti e questo comporta che, almeno in linea di principio, potremmo pensare di accedere ai dati (progetti, versioni dei file, elementi di lavoro e quant’altro) leggendo le tabelle del data base.

Questa tecnica, pur se praticabile, è sconsigliata poiché si può essere certi che la struttura del database (tabelle, viste, stored procedure, etc., etc.) rimanga inalterata nel tempo.

Per tale motivo, il modo migliore per accedere ai dati di Team Foundation Server è di utilizzare proprio l’object model messo a disposizione.

La classe fondamentale per accedere ai servizi esposti da Team Foundation è la classe TeamFoundationServer del namespace Microsoft.TeamFoundation.Client.

Questa classe, mostrata in figura 1, ci consente di eseguire la connessione al server e di richiedere uno dei tanti servizi messi a disposizione.

Figura1

Possiamo osservare che la classe implementa l’interfaccia IServiceProvider ovvero l’interfaccia che consente di definire un meccanismo per il recupero di un oggetto Service, vale a dire un oggetto che offre un supporto personalizzato ad altri oggetti.

E’ grazie a questa interfaccia che possiamo utilizzare del metodo GetService() che ci permette di recuperare uno dei servizi messi a disposizione Team Foundation Server.

Vedremo in seguito come utilizzare il metodo GetService(), per ora limitiamoci a segnalarne l’esistenza e spostiamo l’attenzione su come instaurare una connessione al server di Team Foundation tramite la classe TeamFoundationServer.

Abbiamo due modalità:

  • utilizzando il costruttore della classe (che in alcuni dei suoi overload accetta oggetti ICredentials o ICrdedentialsProvider contenenti le credenziali di connessione);
  • utilizzando la classe TeamFoundationServerFactory che permette di ottenere un’istanza della classe TeamFoundationServer tramite il metodo GetServer().

Esaminiamo in dettaglio la seconda strada, cioè l’utilizzo della TeamFoundationServerFactory, poiché rappresenta la modalità ottimizzata per ottenere un riferimento al server di Team Foundation.

La classe TeamFoundationServerFactory, infatti, gestisce una propria cache interna relativa alle connessioni e alle informazioni sulle stesse. Ogni qual volta che ne è richiesta una con il metodo GetServer(), viene eseguito un controllo se la connessione richiesta è presente o meno tra quelle della cache e, eventualmente, restituita senza effettuarla di nuovo.

Il metodo GetServer() ha due differenti overload uno dei quali prevede, come argomento, il nome del server di Team Foundation, mentre l’altro prevede oltre, a questo, anche un oggetto di tipo ICredentialsProvider che rappresenta l’oggetto da invocare nel caso l’autenticazione di rete del client che esegue la richiesta fallisca.

Vedremo, nella parte riguardante il codice, come gestire l’autenticazione in maniera trasparente per l’utente oppure lasciare allo stesso la responsabilità di digitare username e password.

Nel momento in cui si ha un oggetto di classe TeamFoundationServer (sia con i costruttori della stessa o attraverso l’utilizzo della Factory), è necessario invocare il metodo Authenticate() per avviare la richiesta di autenticazione sul server.

Il metodo utilizza l’oggetto ICredentialsProvider (passato come parametro del costruttore o nel metodo GetServer() factory) per gestire l’autenticazione.

Non appena si è stabilita la connessione al server, è possibile richiedere uno dei servizi disponibili.

In quest’articolo prendiamo in esame solamente i servizi riguardanti la gestione degli elementi di lavoro (workitems).

La classe che ci permette di gestire gli elementi di lavoro è la WorkItemStore (figura 2) che si ottiene dal server con la chiamata:

server.GetService(GetType(WorkItemStore))

Figura2

La classe WorkItemStore si trova nel namespace Microsoft.TeamFoundation.WorkItemTracking.Client e fornisce una serie di metodi che permettono di recuperare, creare o modificare elementi di lavoro presenti in un qualunque progetto all’interno del server di Team Foundation.

La WorkItemStore espone la proprietà Projects (di tipo ProjectCollection) contenente la lista dei progetti presenti all’interno del server (ogni elemento della ProjectCollection è un oggetto Project) e la proprietà FieldDefinitions (di tipo FieldDefinitionCollection) contenente la lista delle definizioni dei campi disponibili nel server per un elemento di lavoro.

Per recuperare l’elenco degli elementi di lavoro all’interno del server e relativi a tutti i progetti o a un particolare progetto, si utilizza il metodo Query() che prevede la possibilità di eseguire una query di tipo custom (con il linguaggio WIQL, in modo analogo a quello che si fa con un database), oppure una delle query presenti nella lista delle query disponibili sul server.

Per recuperare le query predefinite, è possibile utilizzare il metodo GetStoredQuery() che permette di ottenere un oggetto StoredQuery da cui è possibile ricavare la query in linguaggio WIQL che può essere utilizzata all’interno del metodo Query().

Il metodo Query() ritorna un oggetto WorkItemCollection contenente i singoli WorkItem risultanti dalla richiesta (figura 3).

Figura3

La classe WorkItem (presente nel namespace Microsoft.TeamFoundation.WorkItemTracking.Client) incapsula l’elemento di lavoro di Team Foundation Server ed espone tutte le proprietà dello stesso, dal titolo fino ad arrivare allo stato o all’assegnatario dello stesso.

E’ possibile eseguire una query anche limitatamente da uno dei progetti contenuti nella collezione dei progetti di WorkItemStore, in questo caso la query concernerà i soli elementi di lavoro del progetto e non dell’intero server.

Progettiamo il nostro reminder

L’applicativo che realizzeremo ha la caratteristica di avviarsi mostrando un’icona all’interno della tray bar e di avvertire l’utente, tramite un pop-up, nel momento in cui ci si accorge della presenza di nuovi elementi di lavoro.

Il cuore dell’applicativo è una classe, che chiamiamo WorkItemsManager le cui responsabilità sono quelle di connettersi al server di Team Foundation, eseguire un polling all’indirizzo dello stesso, per capire se sono presenti nuovi elementi di lavoro, e gestire tutti i meccanismi di disconnessione e riconnessione verso il server stesso.

I riferimenti a Team Foundation Server, quali nome del server, account di connessione (in termini di username e password) ed eventuale dominio sono memorizzati all’interno del file di configurazione app.config e sono esposti dalla classe WorkItemsManager tramite apposite proprietà.

La figura 4 mostra l’insieme delle classi necessarie per implementare le funzionalità che abbiamo in mente.

Figura4

Connessione ad un server di Team Foundation

Per poter gestire la connessione al server di Team Foundation impostando i dati di connessione da codice e non chiedendoli ogni volta all’utente, abbiamo la necessità di realizzare un CredentialsProvider.

Come già visto in precedenza, un CredentialsProvider (che deve implementare l’interfaccia ICredentialsProvider) è utilizzato dalla TeamFoundationServer nel momento in cui la classe stessa tenta la connessione al server per richiedere le credenziali di accesso da utilizzare.

In questo caso viene richiamato il metodo GetCredentials() che, per inciso, è uno dei due metodi dell’interfaccia ICredentialsProvider, e che ha la responsabilità di restituire un oggetto ICredentials per la connessione.

Nel nostro caso specifico, la classe che si occupa di ciò è la TFSNetworkCredential che, nel metodo GetCredentials(), crea un oggetto NetworkCredential con lo username, la password ed il dominio specificato.

La classe WorkItemsManager, dispone di un metodo Connect() che permette, una volta impostati username, password, domani e nome del server di Team Foundation, di connettersi fisicamente al server.

A livello di codice viene utilizzata una TeamFoundationServerFactory ed un oggetto TFSNetworkCredential, come mostrato di seguito:

Dim icp As ICredentialsProvider = New TFSNetworkCredential(Me.Username, Me.Password, Me.Domain)
Try
    Me._TeamFoundationServer = TeamFoundationServerFactory.GetServer(Me.ServerName, icp)
    Me._TeamFoundationServer.Authenticate()
    RaiseEvent Connected()
Catch ex As Exception
    Me._TeamFoundationServer = Nothing
    RaiseEvent NotConnected()
End Try

Il flusso di esecuzione prevede la creazione di un oggetto di gestione delle credenziali (TFSNetworkCredential nel nostro caso), il recupero di un oggetto TeamFoundationServer tramite la TeamFoundationServerFactory ed il metodo GetServer() (nel nostro caso l’attributo private _TeamFoundationServer), infine la chiamata al metodo Authenticate() per effettuare l’autenticazione vera e propria.

La classe WorkItemsManager dispone di due eventi per segnalare ai client che la utilizzano, l’avvenuta connessione o disconnessione.

Gestire i WorkItems

Stabilita la connessione al server, è necessario implementare un meccanismo di polling che, periodicamente, effettua una query all’indirizzo di Team Foundation Server per recuperare i nuovi ed eventuali elementi di lavoro.

Per implementare questo meccanismo ci viene in aiuto il multithreading.

La classe WorkItemsManager prevede, infatti, un attributo privato di tipo Thread, due metodi per avviare e fermare il thread ed un blocco di codice che effettua il polling.

Concentriamoci sulla parte di codice che implementa l’accesso ai work items.

Se esiste la connessione a Team Foundation Server, viene richiesto il servizio WorkItemStore utilizzando il metodo GetService() della classe TeamFoundationServer, quindi viene eseguita una particolare query con il metodo Query() della classe WorkItemStore e memorizzata la collezione degli elementi di lavoro ritornata con lo stesso.

Dim ItemStore As WorkItemStore = CType(Me._TeamFoundationServer.GetService(GetType(WorkItemStore)), WorkItemStore)

Dim query As String = "SELECT [System.Id], [Microsoft.VSTS.Common.Rank], [System.WorkItemType], [System.State], [System.Title],[System.ChangedDate] FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] <> 'Closed' ORDER BY [System.ChangedDate] desc"

_WorkItems = ItemStore.Query(query)

La select presente nella stringa query è la query in formato WIQL che recupera tutti gli elementi di lavoro assegnati all’utente corrente ([System.AssignedTo] = @me) e non ancora chiusi ([System.State]<>’Closed’).

Recuperato l’elenco degli elementi di lavoro dell’utente e memorizzati nell’attributo _WorkItems (esposto con la proprietà WorkItems), si procede al controllo degli elementi inseriti successivamente all’ultima data di polling. Nel nostro caso eseguiamo una scansione della collezione degli elementi di lavoro controllando la data di modifica (ChangedDate):

Dim iNewWorkItems As Integer = 0
For Each wi As WorkItem In _WorkItems
    If wi.ChangedDate > Me.LastCheckDate Then
        If WorkItem.ChangedDate > lastdate Then
            lastdate = wi.ChangedDate
        End If
        iNewWorkItems += 1
    End If
Next

Avremmo potuto ottenere lo stesso risultato eseguendo la query filtrata per data di cambiamento successiva all’ultima data di controllo memorizzata nella proprietà LastCheckDate.

Se sono trovati dei nuovi elementi di lavoro, viene sollevato un evento NewWorkItems:

If iNewWorkItems > 0 Then
    RaiseEvent NewWorkItems(iNewWorkItems)
End If

Eseguito il controllo per verificare la presenza di nuovi elementi di lavoro, si attende per un periodo di tempo pari al numero di secondi impostato nella proprietà PollingTime prima di ripetere l’operazione di query verso il server.

Da notare che l’attesa non viene implementata tramite una semplice chiamata al metodo Thread.Sleep() ma con un pezzo di codice leggermente più complesso:

For i As Integer = 0 To Me._PollingTime - 1
    System.Threading.Thread.Sleep(1000)
    If Not Me._bThreadAlive Then
        Exit For
    End If
Next

In questo caso effettuiamo un’attesa di un secondo da ripetere per tante volte quante sono le unità impostate nella proprietà PollingTime.

Questo meccanismo ci consente di poter controllare, durante l’attesa, se il thread deve essere chiuso (magari perché il client è stato chiuso dall’utente) e di terminarlo, quindi, in maniera corretta.

Ciò non sarebbe accaduto se avessimo utilizzato lo Sleep(), caso in cui avremmo dovuto attendere il termine della pausa.

Ultima osservazione da proporre prima di passare al client vero e proprio che utilizza la nostra classe, riguarda l’implementazione dell’interfaccia IDisposable da parte di WorkItemsManager.

Nel momento in cui la classe non viene più utilizzata, si deve garantire una corretta chiusura del thread di lavoro ed è per questo motivo che si è scelto di implementare l’interfaccia IDisposable e di demandare al metodo Dispose() la chiusura effettiva del thread di lavoro.

Il client vero e proprio

Analizziamo, ora, il client (molto semplice a dire la verità) che fa uso della classe WorkItemsManager creata nel paragrafo precedente.

L’applicazione client è composta da un eseguibile che si avvia mostrando un’icona nella tray bar (l’icona sarà differente in base allo stato di connessione al server e alla presenza o meno di nuovi items di lavoro), che segnala con un pop up l’arrivo di nuovi elementi di lavoro e che visualizza, a richiesta dell’utente, l’elenco completo dei work items.

Il client è formato da una form principale con un attributo privato di tipo WorkItemsManager.

Nel gestore dell’evento load della form si recuperano le informazioni relative alla connessione verso il server di Team Foundation (presenti nel file di configurazione dell’applicazione stessa):

Dim servername As String = ConfigurationManager.AppSettings("ServerName")
Dim username As String = ConfigurationManager.AppSettings("UserName")
Dim password As String = ConfigurationManager.AppSettings("Password")
Dim domain As String = ConfigurationManager.AppSettings("Domain")

si istanzia la classe WorkItemsManager:

Me._WorkItemManager = New TFSLibrary.WorkItemsManager(servername, username, password, domain)

si “agganciano” gli opportuni gestori di evento sulla classe WorkItemsManager:

AddHandler _WorkItemManager.Connected, AddressOf WorkItemManager_Connected
AddHandler _WorkItemManager.NotConnected, AddressOf WorkItemManager_NotConnected
AddHandler _WorkItemManager.NewWorkItems, AddressOf NewWorkItems

e, finalmente, si avvia il meccanismo di controllo dei nuovi elementi di lavoro:

_WorkItemManager.Start()

I gestori di evento ci permettono di implementare la funzionalità di cambiamento della icona nella tray bar e il pop-up di arrivo nuovi elementi di lavoro, in particolare nei gestori degli eventi Connected() e NotConnected() modifichiamo in maniera opportuna l’icona per segnalare la connessione o la disconnessione al server.

In maniera analoga il gestore di evento NewWorkItems si occupa di visualizzare il pop-up indicante il numero di nuovi elementi di lavoro presenti e modificare l’icona della tray bar per segnalare la presenza degli stessi.

Da segnalare l’utilizzo della classe IconManager che consente di recuperare le icone compilate all’interno dell’assembly principale dell’applicazione grazie all’utilizzo della tecnica Reflection e della funzione:

Private Shared Function GetResourceIcon(ByVal iconName As String) As Icon
    Dim icon As System.Drawing.Icon = Nothing
    Dim asm As System.Reflection.Assembly = Nothing
    Dim st As System.IO.Stream = Nothing
    asm = Reflection.Assembly.GetExecutingAssembly
    Try
        st = asm.GetManifestResourceStream("WorkItemReminder." + iconName)
        icon = New System.Drawing.Icon(st)
    Catch ex As Exception
        icon = Nothing
    Finally
        If Not st Is Nothing Then
            st.Close()
        End If
    End Try
    Return icon
End Function

Le icone utilizzate dell’applicativo, infatti, sono state compilate come risorse embedded dell’assembly e possono essere recuperate dall’assembly stesso tramite la funzione GetManifestResourceStream() applicata all’assembly principale dell’applicazione.

Questa tecnica può essere utilizzata per qualsiasi tipo di file o risorsa che l’applicazione deve utilizzare ma che non si vuole distribuire come file esterno alla stessa (magari perché non si vuole che sia modificato).

Conclusioni

L’articolo non ha la pretesa di esaurire tutte le funzionalità della gestione degli elementi di lavoro in Team Foundation Server ma si prefigge lo scopo di dare spunti che possono essere colti per esplorare l’object model di Team Foundation (abbastanza vasto e complesso). In questo modo sarà possibile, se la situazione lo richiederà, personalizzare, tramite opportune applicazioni, un ambiente di lavoro già molto completo ed esaustivo ma che resta comunque estensibile e configurabile a piacimento e con discreta facilità.

La classe WorkItemManager può rappresentare un punto di partenza per realizzare una propria classe che gestisce, oltre che la presenza di nuovi elementi di lavoro, anche le funzionalità di modifica e inserimento degli stessi con un’interfaccia grafica che non sia necessariamente quella fornita da Visual Studio.

Scaricate l’articolo in formato pdf e il codice sorgente dal seguente link:



Nessun commento: