Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Definiowanie nazw kolumn dla ICompositeUserType za pomocą Fluent NHibernate

Do aktualnego projektu wykorzystuje klasę DateRange z biblioteki MindLib. Klasa ta definiuje w łatwy sposób okres czasu oraz metody do manipulacji okresem. Biblioteka posiada również klasę DateRangeUserType do definicji typu dla NHibernate. Klasa ta dziedziczy pośrednio po ICompositeUserType. Domyślnie na ten typ składają sie dwie kolumny w tabeli bazy danych:

   1: private string[] propertyNames = new string[] {"Start", "End"};
   2: private IType[] propertyTypes = new IType[] {NHibernateUtil.DateTime, NHibernateUtil.DateTime};

Co jeżeli chcielibyśmy wykorzystać dwa razy typ DateRange w naszej encji ? Należy wówczas dla każdego property zdefiniować parę kolumn o różnych nazwach. W pliku mapowania NHibernate wyglądałoby to tak:

   1: <property name="Period" 
   2:           type="MindHarbor.TimeDataUtil.DateRangeUserType,MindHarbor.TimeDataUtil">
   3:     <column name="RangeStart"/>
   4:     <column name="RangeEnd" />
   5:  </property>

… a za pomocą Fluent NHibernate tak:

   1: mapping.Map(x => x.Period).CustomType<DateRangeUserType>()
   2:     .Columns.Add("RangeStart", "RangeEnd");

Niestety to spowoduje błąd mówiący o tym że za dużo jest kolumn zdefiniowanych dla typu DateRangeUserType. Okazuje się że autorzy Fluent NHibernate automatycznie definiują kolumny dla mapowań implementujących ICompositeUserType:

   1: /// <summary>
   2: /// Specifies that a custom type (an implementation of <see cref="IUserType"/>) should be used for this property for mapping it to/from one or more database columns whose format or type doesn't match this .NET property.
   3: /// </summary>
   4: /// <param name="type">A type which implements <see cref="IUserType"/>.</param>
   5: /// <returns>This property mapping to continue the method chain</returns>
   6: public PropertyPart CustomType(Type type)
   7: {
   8:     if (typeof(ICompositeUserType).IsAssignableFrom(type))
   9:         AddColumnsFromCompositeUserType(type);
  10:  
  11:     return CustomType(TypeMapping.GetTypeString(type));
  12: }
  13:  
  14: -- Cut--
  15:  
  16: private void AddColumnsFromCompositeUserType(Type compositeUserType)
  17: {
  18:     var inst = (ICompositeUserType)Activator.CreateInstance(compositeUserType);
  19:  
  20:     foreach (var name in inst.PropertyNames)
  21:     {
  22:         Columns.Add(name);
  23:     }
  24: }

Zatem należy wykasować automatycznie stworzone kolumny a dopiero potem zdefiniować własne.

   1: mapping.Map(x => x.Period).CustomType<DateRangeUserType>()
   2:     .Columns.Clear()
   3:     .Columns.Add("RangeStart", "RangeEnd");
opublikowano przez rod | 0 komentarzy

Spowolnienie VirtualPC przez System Restore

… Jeżeli używacie dysku systemowego (zazwyczaj C: ) do przechowywania swoich wirtualnych maszyn, to możecie odczuwać pewne spowolnienie podczas operacji dyskowych wewnątrz systemu “gościa”. Okazuje się że wasz plik *.vhd będzie ciągle backupowany przez shadow copy. Jednym z rozwiązań jest wyłączenie “System Restore”, ale ja tego nie polecam. Zamiast tego możemy zrobić – Excluding files from Shadow Copy. To samo może tyczyć się innych plików, których snapshotów nie chcemy przetrzymywać. U mnie wpisy wykluczeń w rejestrach wyglądają tak:

$AllVolumes$\VirtualPC\*.* /s
C:\Downloads\*.* /s
$UserProfile$\Downloads\*.* /s

Update:

Po restarcie komputera zdarzyło mi się nadal VirtualPC.exe zapisywał coś do katalogu “System Volume Information”. Dopiero opróżnienie dotychczasowych “restore points” wyeliminowało problem. Być może system w tym katalogu ma nadal zapisane, które pliki ma brać pod uwagę w Shadow Copy.

opublikowano przez rod | 0 komentarzy
Filed under:

Dostęp do repozytoriów git na repo.or.cz z neostrady

Od pewnego czasu, w swoich projektach,  porzucam Subversion na rzecz systemów DVSC takich jak Git, Mercurial czy Bazaar. Na świecie jest wiele projektów z publicznymi repozytoriami umieszczonymi ja takich serwisach jak GitHub, Google Code (od jakiegoś czasu supportuje Mercurial) i inne. W przypadku git jako klienta używam msysgit. Nie jest to jednak 100% port gita. Brakuje mu np. dostępu do repozytoriów za pośrednictwem protokołu https. Jednak istnieje branch, w którym dodana jest już ta możliwość. Repozytorium msysgit znajduje się tu http://repo.or.cz/r/msysgit.git i tu mamy problem. Okazuje się, że TPSA zablokowała jakikolwiek dostęp do domen “or.cz”. Na szczęście git potrafi korzystać z http proxy. Co należy zrobić ? Zakładamy ze znamy adres jakiegoś proxy spoza jurysdykcji TPSA (wystarczy poszukać na google). Do dzieła:

Tworzymy lokalne puste repozytorium git
mkdir msysgit
cd msysgit
git init

Initialized empty Git repository in c:/Projects.Other/msysgit/.git/

Wskazujemy zdalne repozytorium

git remote add origin http://repo.or.cz/r/msysgit.git

Definiujemy proxy

git config remote.origin.proxy "aaa.bbb.ccc.ddd:8080"

Pobieramy repozytorium

git fetch

 

Aktualizacja: 2009-07-19 21:58

Wykonane do tej pory kroki skopiowały nam tylko repozytorium. Aby móc pracować nad kodem należy stworzyć sobie “working copy”. I teraz jest kolejna pułapka. Jeżeli pracujemy nad kodem, który jest tworzony pod różnymi systemami, które mają różne końce linii to warto dać dla naszego lokalnego repozytorium takie ustawienie

git config core.autocrlf false

Teraz czas na stworzenie working copy. Jeżeli zamierzamy traktować nasze zdalne repozytorium jako główne to należy ustawić aby nasz branch master wskazywał na origin-a

git config branch.master.remote origin
git config branch.master.merge refs/heads/master

Teraz juz wystarczy

git pull

 

Wersja uproszczona

Istnieje jednak prostsza metoda. Za pomocą zmiennej systemowej “http_proxy”.

export http_proxy=http://<username>:<password>@<proxy_ip>:<proxy_port>

Teraz juz wystarczy
git clone http://repo.or.cz/r/msysgit.git my_msysgit

I na wszelki wypadek aby po restarcie shella również działało.

git config remote.origin.proxy "aaa.bbb.ccc.ddd:8080"
opublikowano przez rod | 3 komentarzy
Filed under: ,

Bezpieczne mockowanie internal methods.

Trafił mi się dość skomplikowany proces biznesowy, który ma być uruchamiany metodą void Process(). W celu uproszczenia, rozbiłem ciało tej metody na wiele pomniejszych metod.

   1: public void Process()
   2: {
   3:     List<TelesalesCompanyInfoApplication> applicationsFromDb = this.GetApplicationsFromDatabaseForProcessing();
   4:     if (applicationsFromDb == null)
   5:     {
   6:         throw new CriticalBusinessLogicException("Warstwa bazy danych, podczas podawania listy zgloszen nie powinna zwrócic null.");
   7:     }
   8:  
   9:     this.SetLockStatus(applicationsFromDb);
  10:  
  11:     foreach (TelesalesCompanyInfoApplication application in applicationsFromDb)
  12:     {
  13:         if (this.IsOriginalDateOlderThanApplicationDate(application))
  14:         {
  15: ...

Dzięki temu zabiegowi kod stał sie czytelniejszy oraz łatwiej będzie napisać testy jednostkowe. Chciałbym jednak uniknąć upubliczniania wszystkich metod w tej klasie. Do testów wystarczy że ustawimy je jako “internal” oraz zaufamy naszemu projektowi testującemu. Wystarczy w jakimkolwiek pliku dać wpis jak:

   1: [assembly:System.Runtime.CompilerServices.InternalsVisibleTo("Services.UnitTests, PublicKey=A3DS...")]

Teraz bez problemu w naszej klasie testowej możemy testować “pomniejsze”, “internal” metody:

   1: [Test]
   2: public void IsOriginalDateOlderThanApplicationDate_ApplicationVerificationDateIsGreaterThanSurveyEndDate_ReturnTrue()
   3: {
   4:     TelesalesCompanyInfoApplication application = TelesalesCompanyInfoApplicationObjectMother.Make
   5:         .Customize(delegate(TelesalesCompanyInfoApplication x) { x.VerifySourceDocumentDate = DateTime.Now.AddDays(-2); })
   6:         .WithSurveyAs(CompanyInfoSurveyObjectMother.Make.WithDuration(DateTime.Now.AddDays(-10), DateTime.Now.AddDays(-8)));
   7:  
   8:     Assert.GreaterThan(application.VerifySourceDocumentDate, application.Survey.SurveyEnd);
   9:  
  10:     DefaultTelesalesCompanyInfoApplicationImportProcessingService service = new DefaultTelesalesCompanyInfoApplicationImportProcessingService();
  11:     Assert.IsTrue(service.IsOriginalDateOlderThanApplicationDate(application));
  12: }

Natomiast do  przetestowania metody “Process” najlepiej wykorzystać “partial mocking” np. tak:

   1: // Klasa pomocnicza do mockowania
   2: public abstract class MockedTestCase
   3: {
   4:     private MockRepository mockery;
   5:  
   6:     protected MockRepository Mockery
   7:     {
   8:         get { return this.mockery; }
   9:     }
  10:  
  11:     public virtual void SetUp()
  12:     {
  13:         this.mockery = new MockRepository();
  14:     }
  15: }
  16:  
  17: // Bazowa klasa do testowania logiki biznesowej
  18: public class ServiceTestCase<TService> : MockedTestCase where TService : IBaseService
  19: {
  20:     protected Func<TService> sutCreator;
  21:  
  22:     private TService sut;
  23:  
  24:     protected TService Sut
  25:     {
  26:         get { return this.sut; }
  27:         set { this.sut = value; }
  28:     }
  29:  
  30:     public static IUserSession GetStubbedUserSession()
  31:     {
  32:         IUserSession session = new UserSessionStub();
  33:         User sampleUser = new User();
  34:         sampleUser.LoginName = "test";
  35:         session.CurrentUser = sampleUser;
  36:  
  37:         return session;
  38:     }
  39:  
  40:     [SetUp]
  41:     public override void SetUp()
  42:     {
  43:         base.SetUp();
  44:         if (this.sutCreator != null)
  45:         {
  46:             this.Sut = this.sutCreator();
  47:         }
  48:     }
  49:  
  50:     public class UserSessionStub : BaseUserSession
  51:     {
  52:     }
  53: }
  54:  
  55: // Nasza klasa testujaca
  56: [TestFixture]
  57: public class DefaultTelesalesCompanyInfoApplicationImportProcessingServiceTests : ServiceTestCase<DefaultTelesalesCompanyInfoApplicationImportProcessingService>
  58: {
  59:     public DefaultTelesalesCompanyInfoApplicationImportProcessingServiceTests()
  60:     {
  61:         this.sutCreator = delegate { return Mockery.PartialMock<DefaultTelesalesCompanyInfoApplicationImportProcessingService>(); };
  62:     }
  63:  
  64:     [Test]
  65:     public void Process_GetApplicationsFromDatabaseForProcessingReturnsNull_ThrowsCriticalBusinessLogicException()
  66:     {
  67:         using (Mockery.Record())
  68:         {
  69:             Expect
  70:                 .Call(this.Sut.GetApplicationsFromDatabaseForProcessing())
  71:                 .Return(null);
  72:         }
  73:  
  74:         using (Mockery.Playback())
  75:         {
  76:             Assert.Throws<CriticalBusinessLogicException>(delegate { this.Sut.Process(); });
  77:         }
  78:     }
  79: ...

Niestety nie da sie zrobić w taki sposób “partial mocking” dla metod oznaczonych jako “internal”. Musiały by one być “public virtual”, a tego chciałbym uniknąć. Jednym ze sposobów jest ustawienie aby główny projekt “Service” zaufał bibliotece DynamicProxy2 z projektu Castle, która jest niepodpisana. Nie wygląda to zachęcająco. Ale możemy to zrobić pośrednio: “Service” ufa “Service.UnitTests” a ten ufa “DynamicProxy2”. Zatem do “Service.UnitTests” dodajemy:

   1: [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DynamicProxyGenAssembly2")]

oraz tworzymy pośrednia klasę która dziedziczy po naszej klasie testowanej. Należy pamiętać ze musi ona nadpisywać metody które uczestniczą w procesie partial mocking:

   1: public class TrustedDefaultTelesalesCompanyInfoApplicationImportProcessingService : DefaultTelesalesCompanyInfoApplicationImportProcessingService
   2: {
   3:     internal override List<TelesalesCompanyInfoApplication> GetApplicationsFromDatabaseForProcessing()
   4:     {
   5:         return base.GetApplicationsFromDatabaseForProcessing();
   6:     }
   7: }

oraz zmieniamy sposób tworzenia instancji testowanej klasy:

   1: // z
   2:  
   3: public DefaultTelesalesCompanyInfoApplicationImportProcessingServiceTests()
   4: {
   5:     this.sutCreator = delegate { return Mockery.PartialMock<DefaultTelesalesCompanyInfoApplicationImportProcessingService>(); };
   6: }
   7:  
   8: // na
   9:  
  10: public DefaultTelesalesCompanyInfoApplicationImportProcessingServiceTests()
  11: {
  12:     this.sutCreator = delegate { return Mockery.PartialMock<TrustedDefaultTelesalesCompanyInfoApplicationImportProcessingService>(); };
  13: }
opublikowano przez rod | 1 komentarzy
Filed under: ,

Jak uruchomić Mono 2.2 VMware na VirtualPC 2007

Pisząc aplikację .NET czasem chciałbym sprawdzić jak ona chodzi w środowisku Mono pod Linuxem. W tym celu można skorzystać z obrazu wirtualnej maszyny pod VMWare dostępnej stąd. Wszystko pięknie ale ja mam już zainstalowane oprogramowanie MS Virtual PC 2007 SP1 i szczerze powiedziawszy nie chciałbym instalować dodatkowego oprogramowania VMWare Playera. Wiązałoby się to z instalacją dodatkowych driverów na moim i tak obciążonym systemie na notebooku. Jak sobie zatem poradzić ?

  1. Na początek należy przekonwertować VMWare disk image na VHD. Do tego wykorzystałem WinImage. Co prawda wersja shareware ale wyrobiłem się z konwersją w czasie krótszym niż 30 dni :).
  2. Teraz wystarczy stworzyć nowa maszynę wirtualna z systemem “Other” oraz podpiąć przekonwertowany dysk.
  3. W trakcie uruchomienia należy skorzystać z wpisu inicjującego noreplace-paravirt.
  4. Teraz wystarczy dodać tę opcje na stałe do boot-a czyli,
    • Computer –> YaST
    • hasło “mono”
    • System –> Boot Loader
    • Edit i w polu “Optional Kernel Command Line Parameter” dopisać: noreplace-paravirt

Instancje wirtualnych maszyn mogą być mało wydajne na notebookach z nowa generacja procesorów. Chodzi tutaj o dynamiczne obniżanie częstotliwości taktowania procesora. Aby temu zaradzić można wyłączyć poprzez np. wybranie w Viscie trybu High Performance lub ręczne ustawienie “Minimum processor state” na 100% w “Power procesor management”. Alternatywnym rozwiązaniem jest dodanie wpisu do pliku options.xml … jest to opisane tutaj  -> Last resort for performance issues with Virtual PC on laptops.

opublikowano przez rod | 0 komentarzy
Filed under: ,

Intelisense dla jQuery korzystając z MonoRail

Od jakiegoś czasu Microsoft udostępnił fix do Visual Studio 2008, dzięki któremu nie tylko mamy możliwość intelisense dla jQuery ale równiez dla jakichkolwiek bibliotek javascript. Po jego zainstalowaniu wystarczy dodać link do skryptu tak aby Visual Studio mógł go znaleźć. Co jednak zrobić w przypadku kiedy link do skryptu jest generowany w locie lub w danym momencie edytujemy “partial view”, czyli wycinek HTML-a ? Dodatkowo korzystamy z MonoRail-a ?

Wystarczy zrobić prosty trick:

   1: <script type="text/javascript" src="${siteRoot}/content/scripts/jquery.js"></script>
   2: <script type="text/javascript" src="${siteRoot}/content/scripts/jquery.validate.js"></script>
   3: <% if (false): %>
   4: <script type="text/javascript" src="../../content/scripts/jquery.js"></script>
   5: <script type="text/javascript" src="../../content/scripts/jquery.validate.js"></script>
   6: <% end %>
opublikowano przez rod | 0 komentarzy

Ignorowanie testu w MbUnit 3.0.5

Aby zignorować test wystarczy dodać atrybut [Ignore] do deklaracji metody. Co natomiast natomiast zrobić jeżeli decyzje o ignorowaniu wykonania testu chcielibyśmy podjąć dopiero w trakcie jego wykonania ? Zarówno w NUnit jak i MbUnit 2.0 istnieje metoda Assert.Ignore(), która powoduje że test jest ignorowany w momencie jej wywołania.

Niestety MbUnit od wersji 3.0 tej metody już nie ma. Obejściem tego problemu jest rzucenie odpowiedniego Exception. Oto przykład pomocniczej metody testowej, której zadaniem jest sprawdzanie poprawności mapowania długości pola w NHibernate. SQLite nie posiada w ogóle czegoś takiego jak określenie długości pola zatem dla tego typu bazy danych ten test nie miałby sensu. A oto i kod:

   1: protected void AssertPropertyLength(string propertyName, int length)
   2: {
   3:     var driver = ((SessionFactoryImpl)this.Session.SessionFactory).ConnectionProvider.Driver;
   4:     if (driver.GetType().Equals(typeof(SQLite20Driver)))
   5:     {
   6:         throw new SilentTestException(TestOutcome.Ignored, "Test skipped for SQLite");
   7:     }
   8: ...
opublikowano przez rod | 0 komentarzy
Filed under: , ,

Tak blisko, a za daleko

W trakcie pracy nad kodem staram się jak najbardziej ograniczać moje wędrówki między kawiaturą a myszką. W tym celu oprócz ReSharper-a korzystam z ViEmu. Jak sama nazwa wskazuje jest to emulacja edytora Vi pod Visual Studio. Teraz mój gryzoń często leniuchuje przy klawiaturze grzejąc się w blasku monitora :). Mimo tego wciąż moje palce pokonują spore odległości. Sięgając po klawisz Esc oraz strzałki.

Esc w Vi jest używany do wychodzenia z trybu “Insert”, czyli praktycznie non-stop. Alternatywnym rozwiazaniem jest zastosowanie Ctrl+[. Jednak ReSharper też korzysta z tego skrótu. Jak dla mnie najlepszym rozwiązaniem byłoby skorzystanie z klawisza CapsLock zamiast Esc.

Mimo stosowania klawiszy H, J, K, L do poruszania sie po kodzie, czasem zachodzi potrzeba korzystania ze strzałek. Ma to miejsce kiedy wyświetla się nam okienko z Intelisense. Tutaj można by zastosować Alt+N do poruszania się w doł a Alt+P w górę po opcjach intelisense. Tylko jak to zrobić ?

Z pomocą przychodzi nam AutoHotkey. Jest to rezydentny program, który potrafi klawiszom przypisywać różne makra. Makra te sa definiowane w postaci skryptu. W moim wypadku wygląda on tak

#IfWinActive, ,Visual Studio
Capslock::Esc
Alt & p::Send {Up}
Alt & n::Send {Down}

Co ciekawe program ten potrafi konwertować swoje pliki skryptowe do plików exe. Zatem można na obcym komputerze odpalić tak skompilowany skrypt bez instalacji czegokolwiek, aby dalej cieszyć się naszą kombinacją klawiszy.

Update:

Oto mała poprawka do popupów reshapera. Np. w momencie wyszukiwania jakiegos typu. Docelowy skrytpt powinien wygladać tak.

if WinActive("ahk_class Visual Studio") or WinActive("ahk_class JetPopupMenuView")
{
	Capslock::Esc
	Alt & p:: Send {Up}
	Alt & n:: Send {Down}
}
opublikowano przez rod | 0 komentarzy
Filed under: , ,

Remote backup dla SVN na GoogleCode

Backupy dla SVN można wykonać za pomocą komendy "svnadmin dump". Możliwe jest to jedynie wyłącznie kiedy mamy fizyczny dostęp do repozytorium. Co zrobić kiedy nasz projekt jest hostowany ?

  • Należy zrobić lokalny mirror. W tym celu napisałem mały skrypt WSH, który automatyzuje poszczególne kroki. Można go pobrać stąd -> svnmirror.zip oraz uruchomić podając gdzie ma się znajdować lokalne repozytorium a gdzie zdalne. Przykład
C:\SVN>svnmirror C:\SVN\RubyView.Mirror https://dabrowski.daniel@rubyview.googlecode.com/svn
  • Teraz już wystarczy wykonać dump naszego mirror-a czyli:
C:\SVN>svnadmin dump C:\SVN\RubyView.Mirror > RubyView.Mirror.20080821
opublikowano przez rod | 2 komentarzy
Filed under:

Dynamiczne referencje do bibliotek w Visual Studio

Często, w trakcie korzystania z zewnętrznych bibliotek w naszym  projekcie, pojawia się pewien dylemat. Czy dołączone biblioteki, powinny być skompilowane jako "debug" czy jako "release" ?  Zamiast podejmować trudną decyzję zróbmy sobie proste rozwiązanie.

W Automatyzacja projektu z MSBuild-em - 1. Struktura proponowałem przechowywanie "third-party libraries" w katalogu "lib". Teraz dodatkowo zróbmy tam podział na "debug" i "release". Przykład z bibliotekami z IronRuby, które wykorzystuje w aktualnym projekcie:

  • lib
    • net
      • 2.0
        • IronRuby
          • debug
          • release

Struktura jest na tyle oczywista że chyba nie trzeba opisywać.

Teraz w naszym pliku projektu ".csproj" wystarczy tak zmodyfikować referencje do bibiliotek:

<ItemGroup>
  <Reference Include="IronRuby">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>..\..\..\lib\net\2.0\IronRuby\$(Configuration)\IronRuby.dll</HintPath>
  </Reference>
  <Reference Include="IronRuby.Libraries">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>..\..\..\lib\net\2.0\IronRuby\$(Configuration)\IronRuby.Libraries.dll</HintPath>
  </Reference>
  <Reference Include="Microsoft.Scripting">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>..\..\..\lib\net\2.0\IronRuby\$(Configuration)\Microsoft.Scripting.dll</HintPath>
  </Reference>
  <Reference Include="Microsoft.Scripting.Core">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>..\..\..\lib\net\2.0\IronRuby\$(Configuration)\Microsoft.Scripting.Core.dll</HintPath>
  </Reference>
  <Reference Include="System" />
  <Reference Include="System.Data" />
  <Reference Include="System.Xml" />
</ItemGroup>

Jezeli nasz projekt jest kompilowany w trybie "Debug", wówczas kompilacja odbędzie się z wykorzystaniem innych bibliotek z podkatalogu "debug". Analogicznie z "Release".

opublikowano przez rod | 0 komentarzy
Filed under:

Testowanie kompatybilnosci przegladarek dla Helpers w MonoRail

Helpers w MonoRail są często wykorzystywane do generowania kodu Html. Bywają sytuacje kiedy tworzony Html powinien wyglądać inaczej dla różnych przeglądarek. Czasem do wykrycia rodzaju oraz wersji przeglądarki stosuje się System.Web.HttpRequest.Browser, który zwraca klasę System.Web.HttpBrowserCapabilities. Niestety to rozwiązanie ma trzy zasadnicze wady:

  • Trudno jest mockować klasę HttpBrowserCapabilities
  • Rozwiązanie to opiera się na pliku browscap.ini. W momencie uruchamiania aplikacji  z serwera firmy hostingowej, nie mamy żadnej kontroli nad aktualizacja pliku browscap.ini.
  • System.Web.HttpRequest różni się od  Castle.MonoRail.Framework.IRequest przede wszystkim tym, że IRequest nie ma property Browser :) i nie korzysta z HttpBrowserCapabilities :). Całe szczęście.

Pozostaje nam stare dobre rozwiązanie a mianowicie własnoręczne rozpoznawanie "HTTP_USER_AGENT" w naszym Helperze.

public virtual bool IsCompatibleBrowser()
{
    string agent = Context.Request.Params["HTTP_USER_AGENT"];
 
    // IE
    if (agent.IndexOf("MSIE") >= 0 && agent.IndexOf("Windows") >= 0 && agent.IndexOf("Opera") < 0)
    {
        var match = Regex.Match(agent, @"(?<=MSIE )[\d\.]+");
        return (match.Success && float.Parse(match.Value, CultureInfo.InvariantCulture) >= 5.5);
    }
...

Lecz w jaki sposób to testować ? Najprostszym sposobem jest wykorzystanie istniejących we frameworku MonoRail klas typu stub.

using Castle.MonoRail.Framework.Test;
...
// SUT = System Under Test czyli mój helper
private MyHelper _sut;
 
[SetUp]
public void SetUp()
{
    _sut = new MyHelper();
 
    _sut.SetController(new HomeController(), new ControllerContext());
    _sut.SetContext(new StubEngineContext(new StubRequest(), new StubResponse(), new UrlInfo("area", "home", "index", "/app", "sdm")));
    _sut.ServerUtility = new StubServerUtility();
}

Przydadzą nam się linki do dwóch stron, które zawierają zbiór wszelakiej maści "HTTP_USER_AGENT".

Dzięki nim łatwo możemy stworzyć następujące testy

// IE 5.5+ on Windows
[TestCase(@"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; WOW64; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; Media Center PC 5.0; .NET CLR 1.1.4322)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; InfoPath.1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; Dealio Deskball 3.0)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; NeosBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)")]
// FireFox 1.5+
[TestCase(@"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1")]
[TestCase(@"Mozilla/5.0 (X11; U; Linux x86_64; sv-SE; rv:1.8.1.12) Gecko/20080207 Ubuntu/7.10 (gutsy) Firefox/2.0.0.12")]
[TestCase(@"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060614 Fedora/1.5.0.4-1.2.fc5 Firefox/1.5.0.4 pango-text")]
[TestCase(@"Mozilla/5.0 (X11; U; Darwin Power Macintosh; en-US; rv:1.8.0.12) Gecko/20070803 Firefox/1.5.0.12 Fink Community Edition")]
[TestCase(@"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8) Gecko/20051201 Firefox/1.5")]
public void IsCompatibleBrowser_ThisBrowserShouldBeCompatible(string agent)
{
    _sut.Context.Request.Params["HTTP_USER_AGENT"] = agent;
    Assert.That(_sut.IsCompatibleBrowser());
}
 
// IE < 5.5 on Windows
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 4.01; Windows NT 5.0)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; .NET CLR 1.0.3705)")]
// IE on Mac
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.5b1; Mac_PowerPC)")]
[TestCase(@"Mozilla/5.0 (MSIE 7.0; Macintosh; U; SunOS; X11; gu; SV1; InfoPath.2; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648)")]
// FireFox < 1.5
[TestCase(@"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8b4) Gecko/20050908 Firefox/1.4")]
[TestCase(@"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.13) Gecko/20060410 Firefox/1.0.8")]
[TestCase(@"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.2b) Gecko/20020923 Phoenix/0.1")]
public void IsCompatibleBrowser_ThisBrowserShouldNOTBeCompatible(string agent)
{
    _sut.Context.Request.Params["HTTP_USER_AGENT"] = agent;
    Assert.That(!_sut.IsCompatibleBrowser());
}

Nadmienię tylko, że korzystam z wersji NUnit 2.5 Alpha-3 Release, stąd atrybut "TestCase", który jest odpowiednikiem np. "RowTest" w MbUnit.

opublikowano przez rod | 2 komentarzy
Filed under: ,

Automatyzacja projektu z MSBuild-em - 7. Inputs i Outputs, czyli fast & furious

Podczas kompilacji często możemy dostrzec następujący komunikat.

CoreCompile:
Skipping target "CoreCompile" because all output files are up-to-date with respect to the input files.

Jest to efekt funkcjonalnosci budowy przyrostowej - "incremental build". Dzieki niej nasze skrypty mogą być o wiele wydajniejsze. Każdy target może mieć parametry Inputs i Outputs. Przed wykonaniem targetu MSBuild sprawdza timestamp plików w Inputs plikami w Outputs.  I jeżeli Inputs > Outputs, wówczas przystępuje do wykonania zadania a jeżeli Inputs =< Outputs wówczas "pomija" zadanie z uwzględnieniem "output inferral" ... o tym pózniej.

<Target Name="Build" 
    Inputs="@(CSFile)" 
    Outputs="hello.exe">
 
    <Csc
        Sources="@(CSFile)" 
        OutputAssembly="hello.exe"/>
</Target>

Powyższy przykład pokazuje, że kompilacja zostanie wykonana w momencie, kiedy data któregoś z plików "@(CSFile)" będzie większa niż docelowy plik, czyli "hello.exe".

Spróbujmy zmienić nasz testowy target SayHi tak, aby się wykonywał przyrostowo porównująć pliki do skompilowania z plikami juz skompilowanymi.

<Target Name="SayHi" Inputs="@(Compile)" Outputs="$(OutputPath)\$(TargetName).dll">
    <Message Text="Project $(ProjectName) says 'HI' to everyone." Importance="High" />
    <Beep/>
</Target>

Nie jest to do końca idealne rozwiązanie. Czasem np. zmiana zawartosci pliku Resource powinna również wymusić przyrostowe wykonanie. W powyższym przykładzie to się nie stanie gdyż sprawdzamy wyłącznie "@(Compile)". Co zatem powinniśmy brać pod uwagę ? Wystarczy przyjżeć się taskowi "CoreCompile" z "Microsoft.CSharp.Targets".

<Target
    Name="CoreCompile"
    Inputs="$(MSBuildAllProjects);
            @(Compile);                               
            @(_CoreCompileResourceInputs);
            $(ApplicationIcon);
            $(AssemblyOriginatorKeyFile);
            @(ReferencePath);
            @(CompiledLicenseFile);
            @(EmbeddedDocumentation); 
            $(Win32Resource);
            $(Win32Manifest);
            @(CustomAdditionalCompileInputs)"
    Outputs="@(DocFileItem);
             @(IntermediateAssembly);
             @(_DebugSymbolsIntermediatePath);                 
             $(NonExistentFile);
             @(CustomAdditionalCompileOutputs)"
    DependsOnTargets="$(CoreCompileDependsOn)"
>
...

Zatem moglibyśmy skopiować parametry "Inputs" i "Outputs" z "CoreCompile" do "SayHi". Jednak to nie wystarczy. W momencie gdy będziemy chcieli uruchomić "SayHi" po wykonaniu "CoreCompile", wówczas nasz target nigdy nie zostanie wykonany gdyż "Outputs" będa już "up-to-date". Zróbmy sobie zatem naszą własną zmienną - "IsCompileUpToDate".

Output inferral

Mało kto o tym wie, ale istnieje pewna ukryta cecha w "incremental build".

Uwaga: Bez względu na "Inputs" i "Outputs", MSBuild zawsze skanuje target i zawsze wykonuje elementy odpowiedzialene za tworzenie lub zmianę Property i Item.

Cecha ta nazywa się "output inferral" i ma ona niwelować negatywny wplyw modyfikowanych zmiennych w zadaniach pominiętych, na realizację zadań jeszcze nie wykonanych. Zobaczymy to na przykładzie tworzenia naszej pomocniczej zmiennej.

<Target Name="SetIsCompileUpToDate" DependsOnTargets="_InitializeIsCompileUpToDate;_CheckIsCompileUpToDate" />
        
<Target Name="_CheckIsCompileUpToDate"
        Inputs="$(MSBuildAllProjects);
                @(Compile);
                @(_CoreCompileResourceInputs);
                $(ApplicationIcon);
                $(AssemblyOriginatorKeyFile);
                @(ReferencePath);
                @(CompiledLicenseFile);
                @(EmbeddedDocumentation);
                $(Win32Resource);
                $(Win32Manifest);
                @(CustomAdditionalCompileInputs)"
        Outputs="@(DocFileItem);
                @(IntermediateAssembly);
                @(_DebugSymbolsIntermediatePath);
                $(NonExistentFile);
                @(CustomAdditionalCompileOutputs)">
    <CreateProperty Value="false">
        <Output PropertyName="IsCompileUpToDate" TaskParameter="ValueSetByTask"/>
    </CreateProperty>
    <Message Text="_CheckIsCompileUpToDate $(IsCompileUpToDate)" />
</Target>
 
<Target Name="_InitializeIsCompileUpToDate">
    <CreateProperty Value="true" >
        <Output PropertyName="IsCompileUpToDate" TaskParameter="ValueSetByTask" />
    </CreateProperty>
    <Message Text="_InitializeIsCompileUpToDate $(IsCompileUpToDate)" />
</Target>

Początkowo inicjalizujemy naszą zmienną wartością "true", a następnie w zależności od Inputs i Outputs zmieniamy jej wartość na "false". Zgodnie z "output inferral", pomimo iż target "_CheckIsCompileUpToDate" byłby teoretycznie pomijany to zmiennej IsCompileUpToDate i tak nadana by była wartość "false". Od wersji MSBuild 3.5 mamy nowy typ "TaskParameter" a mianowicie "ValueSetByTask", który zastosowałem powyżej. Dzieki niemu omijamy "output inferral" i wszsytko działa tak jak zamierzaliśmy.

Teraz pytanie, w którym momencie powinniśmy uruchomić "SetIsCompileUpToDate" ? Jak zauważylismy w jednym z poprzednich odcinków, dodanie targetu do "CoreCompileDependsOn" nie jest najlepszym rozwiązaniem gdyż np. dodawanie referencji do projektu spod Visual Studio uruchamia target "CoreCompile". Z drugiej strony musimy być pewni że nasze zmienne w Inputs i Outpus są wypełnione przez proces budowy. Na przykład  "_CoreCompileResourceInputs" dopiero powstaje w "_GenerateCompileInputs" w Microsoft.Common.targets. Przyjżyjmy się jak wygląda wogóle target "Compile".

<PropertyGroup>
    <CompileDependsOn>
      ResolveReferences;
      ResolveKeySource;
      SetWin32ManifestProperties;
      _GenerateCompileInputs;
      BeforeCompile;
      _TimeStampBeforeCompile;
      CoreCompile;
      _TimeStampAfterCompile;
      AfterCompile
    </CompileDependsOn>
</PropertyGroup>
<Target
    Name="Compile"
    DependsOnTargets="$(CompileDependsOn)"/>

Teraz już widzimy ... najlepiej w "BeforeCompile". Ten target możemy poprostu nadpisać w naszym Commons.Targets

<Target Name="BeforeCompile" DependsOnTargets="SetIsCompileUpToDate" />

Zatem zróbmy już docelowy refaktoring zarówno dla "SayHi" jak i "IncludeGeneratedAssemblyInfo".

Pamiętacie nasz trick z "Touch" przy generowaniu AssemblyInfo.cs ? Teraz możemy go pominąć, ale aby zachować kompatybilność, być może ktoś nie bedzie chciał korzystać z metody "SetIsCompileUpToDate", zmodyfikujmy go tak ...

<Touch Files="$(AssemblyInfoFile)" Time="2000-01-01" Condition="$(IsCompileUpToDate) == ''" />

... natomiast Commons.Target będzie wyglądał tak ...

<!-- Add additional depends to Build target -->
<PropertyGroup>
    <BuildDependsOn>
        $(BuildDependsOn);
        SayHi;
    </BuildDependsOn>
</PropertyGroup>
...
<Target Name="BeforeCompile" DependsOnTargets="SetIsCompileUpToDate">
    <CallTarget Targets="IncludeGeneratedAssemblyInfo" Condition="$(IsCompileUpToDate) == 'false'" />
</Target>
 
<Target Name="SayHi" Condition="$(IsCompileUpToDate) == 'false'" >
    <Message Text="Project $(ProjectName) says 'HI' to everyone. " Importance="High" />
    <Beep/>
</Target>

Kod do dzisiejszego odcinka dostępny tutaj -> part007.

Na konieć nadmienię że istnieje bardzo ciekawe narzędzie, które nazywa sie MSBuild Profiller. Sposób działania jest bardzo prosty. Opiera się on na własnym Loggerze do MSBuild i po zainstalowaniu uruchamia się go w następujący sposób

MSBuild.exe mybuildfile.proj /t:mytarget /l:MSBuildProfileLogger,MSBuildProfiler,Version=1.0.1.0,Culture=neutral,PublicKeyToken=09544254e89d148c

.. i mamy wówczas taki efekt

opublikowano przez rod | 0 komentarzy
Filed under: ,

Automatyzacja projektu z MSBuild-em - 6. Numer wersji z SVN revision

Tym razem zajmiemy się dynamiczną kompilacją, która wykona się również podczas budowy w Visual Studio. Naszym celem będzie stworzenie pliku "AssemblyInfo.cs" oraz dynamiczne włączenie go do kompilacji. Efektem tego, będzie brak AssemblyInfo w naszej strukturze plików widocznej w "Solution Explorer". Nie będzie to kusiło żadnego z członków zespołu aby go modyfikować. Parametry do jego zawartości będą w centralnym miejscu.

W tym odcinku pojawi się nowy plik - "tools\msbuild\rod.Commons\rod.Commons.Targets". W nim znajdują się taski, które będą opisane poniżej. Używam je we wszystkich swoich projektach, dlatego są wyodrębnione do oddzielnego pliku. W naszym wypadku, równie dobrze jego zawartość można by umieścić w pliku Common.Targets. Ale zamiast tego umieścimy tam tylko Import.

<Import Project="$(MSBuildExtensionsPath)\rod.Commons\rod.Commons.Targets" Condition="$(RodCommonsTargetsIsLoaded) == ''" />

Generowanie AssemblyInfo.cs

Następnym krokiem jest oczywiście "Exclude From Project" dla istniejących AssemblyInfo.cs. Ja dodatkowo oznaczam je jako ignore w SVN property. Pliki AssemblyInfo.cs będziemy generować za pomocą tasku "AssemblyInfo". Parametry zapiszemy w Settings.proj.

<!-- AssemblyInfo Properties -->
<PropertyGroup>
    <AssemblyInfoFile>Properties\AssemblyInfo.cs</AssemblyInfoFile>
    <AssemblyTitle>MySolution - $(AssemblyTitle)</AssemblyTitle>
    <AssemblyDescription>Sample application.</AssemblyDescription>
    <AssemblyCompany>rod</AssemblyCompany>
    <AssemblyCopyright>Copyright 2008 rod</AssemblyCopyright>
    <AssemblyKeyFile>$(RootPath)\MySolution.snk</AssemblyKeyFile>
    <AssemblyProduct>MySolution</AssemblyProduct>
    <AssemblyVersion>1.0.0.0</AssemblyVersion>
</PropertyGroup>

Dodatkowo możemy w każdym z projektów zmodyfikować poszczególne properties. Nagłówki naszych plików projektowych możemy zmienić w następujący sposób:

MyProject.csproj

<!-- Root Path definition relative for actual build file -->
<PropertyGroup>
    <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)\..\..\..\</RootPath>
    <AssemblyTitle>Sample library</AssemblyTitle>
    <AssemblyGuid>F5830C28-699B-4789-AEA4-95AAB38A73CF</AssemblyGuid>
</PropertyGroup>

MyProject.Tests.csproj

<!-- Root Path definition relative for actual build file -->
<PropertyGroup>
    <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)\..\..\..\</RootPath>
    <AssemblyTitle>$(AssemblyTitle) - Unit Tests for Sample library</AssemblyTitle>
</PropertyGroup>

Za wygenerowanie AssemblyInfo.cs odpowiedzialny jest następujący target:

<Target Name="GenerateAssemblyInfo" DependsOnTargets="CalculateAssemblyVersion" >
    <AssemblyInfo CodeLanguage="CS"
        OutputFile="$(AssemblyInfoFile)"
        AssemblyTitle="$(AssemblyTitle)"
        AssemblyDescription="$(AssemblyDescription)"
        AssemblyCompany="$(AssemblyCompany)"
        AssemblyCopyright="$(AssemblyCopyright)"
        AssemblyProduct="$(AssemblyProduct)"
        AssemblyVersion="$(AssemblyVersion)"
        AssemblyFileVersion="$(AssemblyVersion)"
        AssemblyKeyFile="$(AssemblyKeyFile)"
        Guid="$(AssemblyGuid)" />
</Target>

Target "CalculateAssemblyVersion", od którego jest uzależniony "GenerateAssemblyInfo", będzie omówiony później. W poprzednim odcinku dowiedzieliśmy się, że za zbiór plików do kompilacji odpowiada ItemGroup "Compile". Tym razem też go użyjemy:

<Target Name="IncludeGeneratedAssemblyInfo" DependsOnTargets="GenerateAssemblyInfo" Condition="Exists('$(AssemblyInfoFile)')">
    <CreateItem Include="$(AssemblyInfoFile)">
        <Output ItemName="Compile" TaskParameter="Include"/>
    </CreateItem>
    <Touch Files="$(AssemblyInfoFile)" Time="2000-01-01" />
</Target> 

Wywołanie tasku Touch jest swego rodzaju trickiem. AssemblyInfo.cs bedzie generowany przy każdym wywołaniu Build-a. Jak wiemy, jeżeli pliki źródłowe nie zostały zmodyfikowane, wówczas kompilacja podczas budowy jest pomijana. Gdybyśmy nie zastosowali powyższego tasku, wówczas kompilacja odbywałaby się za każdym razem i podczas budowy solution w Visual Studio kompilowały by się wszystkie projekty, nawet te, które nie były zmodyfikowane. Wywołamy powyższy target przed samą budową, wpisując w Common.Targets...

<!-- Add additional depends to Build target -->
<PropertyGroup>
    <BuildDependsOn>
        IncludeGeneratedAssemblyInfo;
        $(BuildDependsOn)
    </BuildDependsOn>
</PropertyGroup>

Dzięki temu plik AssemblyInfo.cs będzie się generował również podczas budowy Visual Studio ... i za pomocą NAnta nie dalibyśmy rady tego uzyskać lub byłoby to dosyć skomplikowane.

Tworzenie numeru wersji na podstawie wartości w pliku tekstowym oraz SVN revision.

W swoich projektach zazwyczaj stosuję następującą strategię wersjonowania 1.0.NumerIteracji.SVNRevision. Numer iteracji zapisuję w pliku w roocie projektu ...

<!-- Helper Files -->
<PropertyGroup>
    <IterationNumberFile Condition=" '$(IterationNumberFile)' == '' ">$(RootPath)\IterationNumber.txt</IterationNumberFile>
</PropertyGroup>

Ten plik może być generowany przez zewnętrzne narzędzie. Dobrym przykładem jest np. numer poprawnie przetestowanego builda przez narzędzie do Continuous Integration. Do pobrania numeru z pliku zastosujemy...

<!-- Gets the iteration number from file -->
<Target Name="GetIterationNumber">
    <!-- Read the the iteration number file contents -->
    <ReadLinesFromFile File="$(IterationNumberFile)">
        <Output TaskParameter="Lines" ItemName="IterationNumberFileContents"/>
    </ReadLinesFromFile>
 
    <!-- Assign file contents to IterationNumber property -->
    <CreateProperty Value="@(IterationNumberFileContents->'%(Identity)')">
        <Output TaskParameter="Value" PropertyName="IterationNumber"/>
    </CreateProperty>
 
    <!-- If tehere is no IterationNumber, set zero -->
    <CreateProperty Value="0" Condition="$(IterationNumber) == ''">
        <Output TaskParameter="Value" PropertyName="IterationNumber"/>
    </CreateProperty>
</Target>

Z SVN revision jest trochę inaczej. Jeżeli dany projekt nie został zmodyfikowany będę nadal chciał kompilować go z SVN revision jego ostatniego commit-u. Ta informacja jest zapisana w "LastChangedRevision". Jeżeli projekt został zmodyfikowany, zastosuje "Revision", które równa się najbliższemu numerowi, który zostanie nadany podczas następnego Commit. Jeżeli ktoś w międzyczasie zrobi własny revision, wówczas ten numer nam wskaże na jego commit. Dobrą praktyką jest zatem zrobienie Commit potem Update a potem Build końcowy.

<!-- Get the revision number of the local working copy -->
<Target Name="GetSvnRevision">
    <SvnVersion LocalPath="$(MSBuildProjectDirectory)" ContinueOnError="true">
        <Output TaskParameter="Modifications" PropertyName="SvnModified" />
    </SvnVersion>
 
    <SvnVersion
        LocalPath="$(MSBuildProjectDirectory)"
        UseLastCommittedRevision="!$(SvnModified)"
        ContinueOnError="true">
        <Output TaskParameter="Revision" PropertyName="SvnRevision"/>
    </SvnVersion>
 
    <PropertyGroup>
        <SvnRevision Condition="$(SvnRevision) == ''">0</SvnRevision>
    </PropertyGroup>
</Target>

Na końcu tasku zabezpieczamy property na wypadek, kiedy jeszcze nie mamy projektu pod kontrolą SVN-u. Ostatnim krokiem jest już złożenie wersji w całość.

<Target Name="CalculateAssemblyVersion" DependsOnTargets="GetIterationNumber;GetSvnRevision">
    <CreateProperty Value="$(AssemblyVersion).$(IterationNumber).$(SvnRevision)">
        <Output TaskParameter="Value" PropertyName="AssemblyVersion"/>
    </CreateProperty>
    <Message Text="Calculated Assembly Version: $(AssemblyVersion)" Importance="normal"/>
</Target>

Należy pamiętać jednak aby zmienić w Settings.proj nasz AssemblyVersion na postać dwucyfrową np.

<AssemblyVersion>1.0</AssemblyVersion>

Kod do dzisiejszego odcinka znajdziecie tutaj -> part006.

Update 2008-07-30

Mała aktualizacja związana z podpisywaniem Assemblies. Już od wersji NET 2.0 assemblies powinno się podpisywać przy wykorzystaniu parametru do kompilatora a nie poprzez wpis w AssemblyInfo.cs. Kwestie bezpieczeństwa. Jeżeli zrobimy to wg starego sposobu, wówczas podczas budowy pojawi się następujące ostrzeżenie.

warning CS1699: Use command line option '/keyfile' or appropriate project settings instead of 'AssemblyKeyFile'

Zatem w Settings.proj dodajemy następujące linie:

<!-- Signing Properties-->
<PropertyGroup>
    <SignAssembly>true</SignAssembly>
    <AssemblyOriginatorKeyFile>$(RootPath)\MySolution.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

... i oczywiście usuwamy property "AssemblyKeyFile", które zadeklarowaliśmy wcześniej w tym odcinku.

opublikowano przez rod | 0 komentarzy

Automatyzacja projektu z MSBuild-em - 5. Makefile z Mono

W tym odcinku chciałbym przedstawić jak można dynamicznie budować kompilację, nie używając do tego w ogóle Visual Studio. W następnym odcinku temat będzie podobny, ale już z uwzględnieniem Visual Studio.

Za przykład posłuży nam projekt, który oryginalne był tworzony pod mono z wykorzystaniem plików Makefile. Mowa tu o FaRetSys aka eithne. Do budowy wykorzystamy źródła wersji 0.4.2. Aby być niezależnym oraz nie modyfikować plików źródłowych w żaden sposób, przyjąłem następującą strukturę:

  • ..
    • eithne - katalog, do którego wrzucamy źródła eithne. Na ten katalog będzie wskazywać property $(SourcePath)
    • eithne.msbuild - katalog z plikami, które my będziemy tworzyć oraz dostarczać - $(RootPath)
      • build - zostanie utworzony dynamicznie i tu znajdzie się skompilowana aplikacja
      • lib - katalog, do którego wrzucamy potrzebne biblioteki - $(LibraryPath)
      • tools - raczej oczywiste

Projekt będziemy budować za pomocą "csc.exe" czyli .NET Framework a nie Mono. Do tego celu wykorzystamy oczywiście Microsoft.CSharp.targets. Jako pierwsze skompilujemy źródła IPlugin. Makefile do tych źródeł wygląda następująco.

MCS = mcs
 
TARGET = ../IPlugin.dll
 
IPLUGIN = \
    BPP.cs \
    CommSocket.cs \
    Config.cs \
    DialogMessage.cs \
    DialogQuestion.cs \
    GConfConfig.cs \
    IBlock.cs \
    ICommImage.cs \
    ICommResult.cs \
    IConfig.cs \
    IFactory.cs \
    IImage.cs \
    IInfo.cs \
    IInPlugin.cs \
    IOutPlugin.cs \
    IPlugin.cs \
    IResult.cs \
    IType.cs \
    PluginException.cs \
    Program.cs \
    RegistryConfig.cs \
    ResultSorter.cs \
    Utility.cs
 
RESOURCES = \
    DialogMessage.glade \
    DialogQuestion.glade
 
RESFILES = $(addprefix resources/,$(RESOURCES))
RESCMD = $(addprefix -resource:,$(RESFILES))
 
all: $(TARGET)
 
$(TARGET): $(IPLUGIN) $(RESFILES)
    $(MCS) $(IPLUGIN) $(RESCMD) -out:$(TARGET) -target:library -r:Mono.Posix -unsafe -debug -pkg:gconf-sharp-2.0 -pkg:gtk-sharp-2.0 -pkg:glade-sharp-2.0
 
clean:
    rm -f $(TARGET) $(TARGET).mdb

Jak widać z tego pliku, musimy skompilować "library" z wyszczególnionych plików .cs, załączyć pliki resources oraz stworzyć referencje do m.in. gtk-sharp itp. Za naszą budowę tej biblioteki będzie odpowiadał plik "IPlugin.proj", który umieścimy w $(RootPath).

Źródła do kompilacji

Visual Studio zazwyczaj dodaje pliki do kompilacji pojedynczo. W naszym przypadku wszystkie pliki znajdują się w jednym miejscu więc bez obaw robimy następujący ItemGroup.

<ItemGroup>
    <Compile Include="$(SourcePath)\IPlugin\*.cs" />
</ItemGroup>

EmbeddedResource

Biblioteka ma zawierać dwa pliki resource "DialogMessage.glade" oraz "DialogQuestion.glade". Cóż prostszego ...

<ItemGroup>
    <EmbeddedResource Include="$(SourcePath)\IPlugin\resources\*.glade" />
</ItemGroup>

... i tutaj mamy pierwszą pułapkę. Kod programu odwołuje się do naszych resources w następujący sposób.

Glade.XML gxml = new Glade.XML(Assembly.GetExecutingAssembly()
                            , "DialogMessage.glade"
                            , "DialogMessageWindow"
                            , null);

Aby taki kod mógł zadziałać musimy dodać metatag LogicalName, czyli nasz kawałek definiujący resource w projekcie powinien wyglądać tak ...

<ItemGroup>
    <EmbeddedResource Include="$(SourcePath)\IPlugin\resources\DialogMessage.glade">
        <LogicalName>DialogMessage.glade</LogicalName>
    </EmbeddedResource>
</ItemGroup>

Projekt IPlugin wymaga dwóch plików jako resources ale w trakcie budowy Eithne.exe będziemy potrzebować ich prawie 50. Dodamy je zatem dynamicznie. Do tego potrzebujemy dodatkowy ItemGroup

<ItemGroup>
    <ResourcesToEmbed Include="$(SourcePath)\IPlugin\resources\*.glade" />
</ItemGroup>

Teraz wygenerujemy EmbeddedResource z metatagiem LogicalName za pomocą następującego zadania, które jako ogólnodostępne umieścimy je w Common.Targets

<Target Name="GenerateEmbeddedResources">
    <CreateItem Include="@(ResourcesToEmbed)" AdditionalMetadata="LogicalName=%(ResourcesToEmbed.FileName)%(ResourcesToEmbed.Extension)">
        <Output ItemName="EmbeddedResource" TaskParameter="Include"/>
    </CreateItem>
</Target>

Referencje

Biblioteki potraktujmy jeszcze bardziej brutalnie :). Dodamy je wszystkie i zamiast tworzyć czegoś na styl ...

<Reference Include="Mono.Posix">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>..\..\lib\Mono.Posix.dll</HintPath>
</Reference>

... skorzystamy z zadania, które umieszczamy w Common.Targets ...

<Target Name="GenerateReferencesFromLibrary">
    <ItemGroup>
        <Libraries Include="$(LibraryPath)\*.dll"/>
    </ItemGroup>
    
    <CreateItem Include="@(Libraries.FileName)" AdditionalMetadata="HintPath=%(Libraries.Identity)">
        <Output ItemName="Reference" TaskParameter="Include"/>
    </CreateItem>
</Target>

Zadania "GenerateEmbeddedResources" oraz "GenerateReferencesFromLibrary" uruchamiany tuż przed budową czyli w pliku Common.Targets modyfikujemy property "BuildDependsOn" w taki sposób

<!-- Add additional depends to Build target -->
<PropertyGroup>
    <BuildDependsOn>
        GenerateReferencesFromLibrary;
        GenerateEmbeddedResources;
        $(BuildDependsOn)
    </BuildDependsOn>
</PropertyGroup>

Pozostałe parametry kompilacji

Ostateczny wygląd IPlugin.proj jest następujący ...

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!-- Root Path definition relative for actual build file -->
    <PropertyGroup>
        <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)</RootPath>
    </PropertyGroup>
    <Import Project="$(RootPath)\Settings.proj" />
    <PropertyGroup>
        <OutputType>Library</OutputType>
        <AssemblyName>IPlugin</AssemblyName>
    </PropertyGroup>
    <ItemGroup>
        <Reference Include="System" />
        <Reference Include="System.Data" />
        <Reference Include="System.Xml" />
    </ItemGroup>
    <ItemGroup>
        <Compile Include="$(SourcePath)\IPlugin\*.cs" />
    </ItemGroup>
    <ItemGroup>
        <ResourcesToEmbed Include="$(SourcePath)\IPlugin\resources\*.glade" />
    </ItemGroup>
    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
    <Import Project="$(RootPath)\Common.Targets" />
</Project>

OutputType definiuje nam czym mają być skompilowane assembly, w tym wypadku jako biblioteka dll, a AssemblyName jaka ma być jego nazwa. Pozostałe parametry kompilacji znajdują się w pliku Settings.proj, gdyż można powiedzieć, że są wspólne dla pozostałych projektów również.

<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <SchemaVersion>2.0</SchemaVersion>
    <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>$(BuildPath)\Debug\bin\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <BaseIntermediateOutputPath>$(BuildPath)\temp\Debug\obj\</BaseIntermediateOutputPath>
    <IntermediateOutputPath>$(BuildPath)\temp\Debug\obj\</IntermediateOutputPath>
    <UseHostCompilerIfAvailable>true</UseHostCompilerIfAvailable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>$(BuildPath)\Release\bin\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <BaseIntermediateOutputPath>$(BuildPath)\temp\Release\obj\</BaseIntermediateOutputPath>
    <IntermediateOutputPath>$(BuildPath)\temp\Release\obj\</IntermediateOutputPath>
    <UseHostCompilerIfAvailable>true</UseHostCompilerIfAvailable>
</PropertyGroup>

Zwróćcie uwagę na ustawienie true dla AllowUnsafeBlocks, które jest wymagane w tym wypadku do kompilacji oraz na to że pliki automatycznie są wysyłane do odpowiedniego podkatalogu w katalogu "build" czyli $(BuildPath).

Circular reference

Jednak istnieje jedno zagrożenie. Plik Settings.proj jest zarówno importowany na początku pliku IPlugin.proj jak i na początku Commons.Target. Oznacza to, że przypisywanie zmiennych w projektach odbywa się w następującej kolejności:

  1. Settings.proj
  2. IPlugin.Proj
  3. Settings.proj
  4. Common.Targets

Jeżeli Setting.proj ustawia jakąś wartość zmiennej ABC, a potem IPlugin.proj zmienia tę wartość, to zanim dotrze ona do Common.Targets, z powrotem zostanie zamieniona na wartość z Settings.proj. Jest to w większości wypadków efekt niepożądany i nawet msbuild nas o tym informuje stosownym komunikatem

C:\Projects\eithne.msbuild>msbuild IPlugin.proj
...
C:\Projects\eithne.msbuild\Common.Targets(9,10): warning MSB4011: There is a circular
reference involving the import of file "C:\Projects\eithne.msbuild\Settings.proj". 
This file may have been imported more than once, or you may have attempted to import
the main project file. All except the first instance of this file will be ignored.
...

Aby się przed tym uchronić należy nadać warunek przed kolejnym importem najlepiej oparty o jakąś zmienną, która jest zdefiniowana tylko w pliku Settings.proj i która nie będziemy zazwyczaj modyfikować przy użyciu linii poleceń. Zatem import w pliku Common.Target powinien wyglądać tak...

<Import Project="Settings.proj" Condition="$(ToolsPath) == ''"/>

Budowa pliku Exe

Budowa gdk-cairo.dll wygląda tak samo jak IPlugin.proj. Służy od tego plik Gdk-Cairo.proj.

Za plik exe odpowiadać będzie Eithne.proj. W stosunku do pozostałych plików proj różni się typem budowanego assembly ...

<PropertyGroup>
    <OutputType>WinExe</OutputType>
    <AssemblyName>Eithne</AssemblyName>
</PropertyGroup>

... większej ilości resources ...

<ItemGroup>
    <ResourcesToEmbed Include="$(SourcePath)\resources\*.glade"/>
    <ResourcesToEmbed Include="$(SourcePath)\resources\pixmaps\*.png"/>
</ItemGroup>

... wykluczenia pliku gdk-cairo.cs z kompilacji ...

<ItemGroup>
    <Compile Include="$(SourcePath)\*.cs" Exclude="$(SourcePath)\gdk-cairo.cs" />
</ItemGroup>

... oraz ustawienia zależności pomiędzy pozostałymi projektami czyli ...

<ItemGroup>
    <ProjectReference Include="$(RootPath)\IPlugin.proj">
    </ProjectReference>
    <ProjectReference Include="$(RootPath)\Gdk-Cairo.proj">
    </ProjectReference>
</ItemGroup>

Teraz kompilując projekt Eithne.proj, pozostałe dwa projekty również zostaną zbudowane.

Kompilacja hurtowa pluginów

Źródła pluginów znajdują się w katalogu Plugins, który jest wskazywany przez zmienną $(PluginsSourcePath). Jest ich 28 i oznacza to 28 plików Makefile. Czy również oznacza to że musimy robić 28 plików .proj ? Niekoniecznie. Wszystkie te pluginy tak naprawdę różnią się nazwą zatem możemy zrobić projekt w stylu szablonu. Nazwijmy go PluginTemplate.proj. Oto on ...

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!-- Root Path definition relative for actual build file -->
    <PropertyGroup>
        <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)</RootPath>
    </PropertyGroup>
    <Import Project="$(RootPath)\Settings.proj" />
    <PropertyGroup>
        <ProjectName>$(PluginName)</ProjectName>
        <OutputType>Library</OutputType>
        <AssemblyName>$(PluginName)</AssemblyName>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
        <OutputPath>$(BuildPath)\temp\Debug\bin\Plugins\</OutputPath>
        <DeployPath>$(BuildPath)\Debug\bin\Plugins</DeployPath>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
        <OutputPath>$(BuildPath)\temp\Release\bin\Plugins\</OutputPath>
        <DeployPath>$(BuildPath)\Release\bin\Plugins</DeployPath>
    </PropertyGroup>
    <ItemGroup>
        <Reference Include="System" />
        <Reference Include="System.Data" />
        <Reference Include="System.Xml" />
    </ItemGroup>
    <ItemGroup>
        <ProjectReference Include="$(RootPath)\IPlugin.proj"/>
    </ItemGroup>
    <ItemGroup>
        <Compile Include="$(PluginsSourcePath)\$(PluginName)\*.cs" />
    </ItemGroup>
    <ItemGroup>
        <ResourcesToEmbed Include="$(PluginsSourcePath)\$(PluginName)\resources\*.*" />
    </ItemGroup>
    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
    <Import Project="$(RootPath)\Common.Targets" />
    <Target Name="AfterBuild">
        <ItemGroup>
            <BuildFiles Include="$(TargetDir)\$(TargetName).*" ></BuildFiles>
        </ItemGroup>
        <Copy SourceFiles="@(BuildFiles)" DestinationFolder="$(DeployPath)" ContinueOnError="true"/>
    </Target>    
    
</Project>

Jak widać, najistotniejszy jest parametr $(PluginName). Dzięki niemu możemy budować plugin w następujący sposób.

C:\Projects\eithne.msbuild> msbuild PluginTemplate.proj /p:PluginName=Best

Jedyną różnicą w porównaniu od poprzednich projektów jest prymitywny deployment.  OutputPath musi wskazywać na jakąś tymczasową lokalizację a potem gotowe assemblies muszą być kopiowane do nowej lokalizacji. Jeżeli tego nie zrobimy zadziała wówczas target "IncrementalClean", który nam wyczyści z "OutputPath" pliki z poprzednio budowanego plugina. Dzieję się tak gdyż tak naprawdę projekt o jednej nazwie PluginTemplate.proj buduje assemblies za każdym razem o innych nazwach więc traktuje poprzednio budowane pliki jako obce. Ale ten prosty deployment umieszczony w "AfterBuild" chroni nas przed tym.

Dzieki takiemu rozwiazaniu, dołożenie przez programistę nowego pluginu nie powoduje zmiany plików proj. Wystarczy, że umieści go w nowym podkatalogu. Mamy coś w stylu Convention over Configuration.

Teraz już pozostaje zbudować jednym poleceniem wszystkie pluginy. Zrobimy to przy wykorzystaniu nowego pliku Plugins.proj, który wygląda tak...

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!-- Root Path definition relative for actual build file -->
    <PropertyGroup>
        <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)</RootPath>
    </PropertyGroup>
    <Import Project="$(RootPath)\Settings.proj" />
    
    <ItemGroup>
        <PluginsToBuild Include="$(PluginsSourcePath)\**" Exclude="$(PluginsSourcePath)\**\resources\*;$(PluginsSourcePath)\*" ></PluginsToBuild>
    </ItemGroup>
 
    <!-- Import 3rd party targets -->
    <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
 
    <Target Name="Build"  DependsOnTargets="GetPluginNames">
        <MSBuild Projects="PluginTemplate.proj" Targets="Build" Properties="PluginName=%(PluginNames.Identity)"></MSBuild>
    </Target>
    
    <Target Name="GetPluginNames">
        <RegexReplace Input="%(PluginsToBuild.RecursiveDir)" Expression="\\" Replacement="" Count="1">
            <Output ItemName ="PluginNames" TaskParameter="Output" />
        </RegexReplace>
    </Target>
</Project>

W zadaniu "GetPluginNames" tworzymy listę nazw podkatalogów, którą później wykorzystujemy do uruchomienia tasku MSBuild  budującego projekt PluginTemplate.proj tyle razy ile jest podkatalogów.

Podsumowanie

Gotowe rozwiązanie można pobrać stąd -> eithne.msbuild.zip. Nie zawiera ono bibliotek wymaganych do kompilacji i uruchomienia, typu mono, gdk itp. Dla chętnych biblioteki są w oddzielnym pliku eithne.msbuild.lib.zip lub można dostarczyć je samemu kopiując do katalogu "lib".

Jak widać do naszej budowy potrzebujemy mniej własnych plików .proj aniżeli plików Makefile w oryginale. Aby rozkoszować się aplikacją zbudowaną na Windowsach bez użycia Mono wystarczy ...

C:\Projects\eithne.msbuild> msbuild Eithne.proj
C:\Projects\eithne.msbuild> msbuild Plugins.proj
C:\Projects\eithne.msbuild> build\Debug\bin\Eithne.exe
opublikowano przez rod | 0 komentarzy

Resize Form czyli WinForms-y okiem laika

W WinFormsach programuje zupełnie sporadycznie. Wole warstwy domenowe, serwisowe, bazodanowe i prezentacyjne (w modelach MVP). Widoki toleruje tylko pod MonoRail i ASP.NET. Ale WinForms ? .... e to nie dla mnie. Niestety życie bywa brutalne.  W moim aktualnym projekcie integruje system finansowo księgowy z danymi dostarczanymi z działu aktuarialnego. Procesem przetwarzania danych kieruje aplikacja WinForms-owa. Prosty wygląd w postaci zakładek, gdzie dwie zakładki posiadają kontrolki na wprowadzenie parametrów. Natomiast jedna posiada textbox z zawartością logu, a jeszcze inna ReportViewer z Microsoft Reporting.

Poprzedni programista, który projektował UI, nie uwzględnił tego aby Form był w pełni resizable a wraz z nim textbox z zawartością logu oraz ReportViewer. Mimo, że program potrafi wykonywać skomplikowane przeliczenia księgowe wg. zawiłych reguł biznesowych, łącznie z importem do systemu to brak ładnego powiększania okienka może spowodować że projekt zostanie odrzucony. Dla klienta ważniejsze jest to co widzi aniżeli to co siedzi w środku. To normalne. Lecz niestety zadanie zrobienia resizable form spadło na mnie. No to zaczynamy. www.google.com ... "form c# resize child controls" ... press "search" ... i w sumie nic ciekawego nie znalazłem. No dobra filozofia jest przecież banalna. Jak powiększę okno o powiedzmy 100 px to pozostałe kontrolki też należy powiększyć o te 100 px. Piece of cake. Hmm, ale jak to zrobić i się nie narobić i żeby na przyszłość można było w łatwy sposób dokładać następne kontrolki, które mają reagować tak samo na powiększenie okna.

Na początek należy przechować jaka jest różnica w wysokości i szerokości pomiędzy kontrolkami a oknem. Początkowo do przechowania WidthDifference i HeightDifference zrobiłem własną strukturę, ale szybko okazało się ze już jest taka struktura - Size, wiec postanowiłem ją wykorzystać. Końcowe rozwiązanie mojego problemu jest następujące.

Deklarujemy pole ...

private IDictionary<Control, Size> _sizeDifferences = new Dictionary<Control, Size>();

... podczas ładowania okna zapamiętujemy różnice taką metodą ...

private void RememberInitialSizeDifferences()
{
    _sizeDifferences.Add(MainTabControl, this.Size - MainTabControl.Size);
    _sizeDifferences.Add(CurrentLogTextBox, this.Size - CurrentLogTextBox.Size);
    _sizeDifferences.Add(ImportSummaryReportViewer, this.Size - ImportSummaryReportViewer.Size);
}

... dodajemy metodę do eventu Resize dla obiektu Form z taką zawartością ...

private void MainWindows_Resize(object sender, EventArgs e)
{
    foreach (var pair in _sizeDifferences)
        pair.Key.Size = this.Size - pair.Value;
}

Szybko, prosto i przyjemnie. Można się jeszcze pokusić o bardziej uniwersalne rozwiązanie i aby jakism fajnym atrybutem dekorować te kontrolki, które mają się powiększać wraz z oknem o tę sama ilośc pikseli co samo okno. Ale ... WinFormsy to nie moja działka.

opublikowano przez rod | 5 komentarzy
Filed under:
Więcej wypowiedzi Następna strona »