Enkel integrasjon mot TeamCity


onsdag 15. april 2009 Diverse prosjekter XML Jobb

Du tror det kanskje ikke, men versjon 3 av ContikiCenter er sluppet - bare 14 dager etter versjon 1. Dette er en WPF-applikasjon jeg utvikler som står sentralt plassert i resepsjonen der jeg jobber, og viser noen sentrale data om statusen i CMA Contiki's R&D avdeling.

ContikiCenter TeamCity integration 1
Contiki Center versjon 3.0

Fra før har jeg laget en modul for personellstatus, og en som viser status på vårt nattlige CruiseControl-bygg som produserer installasjonspakker m.m. (litt om den modulen her). Til versjon 3 har jeg utviklet en integrasjon til TeamCity, og her skal jeg fortelle litt om den og dele noen WPF-erfaringer etc.

TeamCity er en server for kontinuerlig integrasjon (CI) fra JetBrains, selskapet bak ReSharper. Hver gang vi sjekker inn endringer i kildekodesystemet vårt, fyrer TeamCity igang automatisk kompilering og kjøring av eventuelle enhets- og integrasjonstester. Kontinuerlig integrasjon er et uvurderlig hjelpemiddel når man jobber i felleskap på samme produkt.

Advarsel: WPF-koden i denne artikkelen utgjør på ingen måte best practice. Jeg har enda ikke lært meg MVVM, og er ikke så flink til å separere logikk og visningskode. Modulene mine er derimot så enkle at dette ikke har vært nødvenig, og jeg håper man kan finne et og annet tips av verdi likevel.

Modulen jeg har laget for ContikiCenter lister statusen på de viktigste CI-prosjektene vi har kjørende. TeamCity tilbyr Atom-feeds med diverse informasjon (Atom er nesten det samme som RSS), og det er dette jeg har brukt for å lage integrasjonen. Med LINQ to XML er det enkelt å hente feeden, parse den og opprette instanser av klassen Build som jeg har laget for å holde informasjon om et TeamCity-bygg. Det gjør jeg på denne måten:

var xml = XDocument.Load(_feedUrl);

XNamespace xmlns = "http://www.w3.org/2005/Atom";

var entries = from xElem in xml.Descendants(xmlns + "entry")

              select new Build

              {

                  Name = BuildParser.ExtractName(xElem.Element(xmlns + "title").Value),

                  BuildTime = BuildParser.ExtractTime(xElem.Element(xmlns + "updated").Value),

                  StatusDescription = xElem.Element(xmlns + "author").Element(xmlns + "name").Value,

              };

BuildParser er en enkel hjelpeklasse for å parse ut deler av informasjonen jeg trenger. Selve modulen som skal vise byggene har følgende code behind:

   24 public partial class TeamCityDefaultView : UserControl

   25 {

   26     private string _feedUrl;

   27     private DispatcherTimer _timer;

   28     private ObservableCollection<Build> _builds = new ObservableCollection<Build>();

   29

   30     public ObservableCollection<Build> Builds

   31     {

   32         get

   33         {

   34             return _builds;

   35         }

   36     }

   37

   38     public TeamCityDefaultView()

   39     {

   40         _feedUrl = ConfigurationManager.AppSettings["TeamCityFeed"];

   41         FetchBuilds();

   42

   43         InitializeComponent();

   44

   45         _timer = new DispatcherTimer();

   46         _timer.Interval = new TimeSpan(0, 1, 0);

   47         _timer.Tick += new EventHandler(_timer_Tick);

   48         _timer.Start();

   49     }

   50

   51     void _timer_Tick(object sender, EventArgs e)

   52     {

   53         FetchBuilds();

   54     }

   55

   56     private void FetchBuilds()

   57     {

   58         var teamCityFeed = new TeamCityFeed(_feedUrl);

   59         teamCityFeed.Load();

   60

   61         _builds.Clear();

   62         foreach (var item in teamCityFeed)

   63         {

   64             _builds.Add(item);

   65         }

   66     }

   67 }

Propertien Builds er en ObservableCollection som til enhver tid inneholder alle byggene fra Atom-feeden. I XAML-delen av kontrollen kan jeg binde mot denne, og brukergrensesnittet vil automatisk oppdatere seg om inneholdet i kolleksjonen oppdaterer seg.

I konstruktøren setter jeg opp en DispatcherTimer til å tikke én gang i minuttet, og for hvert tikk henter jeg feeden på nytt og oppdaterer Builds-properien. TeamCityFeed er klassen min som henter og parser feeden.

Resultatet er modulen du ser nedenfor, som viser navnet og statusbeskrivelsen for hver bygg.

ContikiCenter TeamCity integration 2

Grensesnittet består av to ulike WPF-kontroller. TeamCityDefaultView er den blå, store boksen. DataContext er satt til å peke på seg selv (ved hjelp av den merkelige syntax'en på linje 5 (se nedenfor)), og slik vil den få tilgang til Builds-propertien. TeamCityDefaultView inneholder videre en ItemsControl, som mer eller mindre tilsvarer en Repeater i ASP.NET (om du er kjent med det). Og det er i den kontrollen vi sier at ItemsSource skal bindes mot Builds.

Grensesnittet for hvert enkelt bygg defineres av en annen WPF-kontroll jeg har kalt BuildControl. For å bruke den må jeg inkludere namespacet den ligger i, og det har jeg gjort i linje 3. Ved å sette DataContext={Binding} vil hver kontroll som opprettes på bakrunn av Builds-kolleksjonen motta én Build-instans.

ContikiCenter TeamCity integration 4
Klikk her for større bilde

XAML'en til BuildControl ser du nedenfor. Den inneholder enkelt og greit to tekstblokker som bindes mot henholdsvis Name og StatusDescription fra Build-instansen som er datakonteksten til kontrollen.

ContikiCenter TeamCity integration 5
Klikk her for større bilde

Nå gjenstår det bare en ting: Om en build feiler ønsker jeg at det skal bli veldig synlig for dem som går forbi statustavlen.., jeg vil at den skal skrike rødt. For å få til det overstyrer jeg OnRender i BuildControl-komponenten, henter ut Build-instansen, og sjekker statusbeskrivelsen. Om beskrivelsen er "failed build" overstyrer jeg utseende på Border-kontrollen, som er ansvarlig for hvordan raden ser ut:

   28 protected override void OnRender(DrawingContext drawingContext)

   29 {

   30     Build build = DataContext as Build;

   31

   32     if (build != null)

   33     {

   34         if (IsFailedBuild(build))

   35         {

   36             RadialGradientBrush failedGradientBrush = new RadialGradientBrush()

   37             {

   38                 GradientOrigin = new Point(1, 1.5),

   39                 SpreadMethod = GradientSpreadMethod.Pad,

   40             };

   41             failedGradientBrush.GradientStops.Add(

   42                 new GradientStop(

   43                     Color.FromArgb(0xFF, 0xFF, 0x93, 0x4A), 0));

   44             failedGradientBrush.GradientStops.Add(

   45                 new GradientStop(

   46                     Color.FromArgb(0xFF, 0xFF, 0x19, 0x0B), 1.3));

   47             _border.Background = failedGradientBrush;

   48             _border.Opacity = 1;

   49         }

   50     }

   51     base.OnRender(drawingContext);

   52 }

   53

   54 public bool IsFailedBuild(Build build)

   55 {

   56     return build.StatusDescription.ToLower().Equals("failed build");

   57 }

Borderen (og dens innhold) har jeg også per default satt til å være gjennomsiktig. Om bygget feiler setter jeg opacity til 1, slik at den blir ekstra synlig. Og sånn blir resultatet:

ContikiCenter TeamCity integration 3

Jeg måtte fremprovosere en feil for å få tatt dette snapshot'et, siden byggene våre er så stabile :)

Håper du fant noe av interesse her. Har du noen kommentarer til mine ynkelige forsøk på bruk av WPF så setter jeg umåtelig pris på en kommentar eller tre.

Knagger: , ,


comments powered by Disqus