Moze jest tu cos fajnego

W nawiązaniu do postu Jakuba, podejmując tę zabawe, chciałem oznajmić że:

Może coś napisze dziś, a może jutro, a może później. Nie żeby to pewnie kogoś zainteresowało, ale "kolorytu"*) doda

;-)

*) To sympatyczne określenie zaczerpnięte od Michała Grzegorzewskiego. Michał! dołącz się do kolorowania... Więc chodź pomaluj nasz świat/Na żółto i na niebiesko...

Zadanie - Komunikacja klient-serwer

Arek próbuje zacząć bardziej praktyczne podejście do tematów omawianych na wg.net. Jako ze pomysł mi się podoba, to może teraz ja opisze pewien problem na który się natknąłem.

Problem

Mamy układ klient-serwer z komunikacja po TCP/IP. Czyli serwer otwiera gniazdko (Socket lub TcpListener), klient łączy się, następuje wymiana wiadomości i klient się rozłącza. A teraz więcej szczegółów.

  1. Wiadomość to poprawny dokument XML, można założyć na potrzeby tego przykładu ze to tylko znaki ASCII wiec mapowanie wiadomość ciąg bajtów jest trywialne.
  2. Wymiana wiadomości przebiega tak ze klient wysyła wiadomość do serwera i zaczyna słuchać odpowiedzi. Potem wysyła kolejna wiadomość i nasłuchuje odpowiedzi.
  3. Serwer robi to samo tylko symetrycznie. Najpierw przyjmuje połączenie, potem wczytuje wiadomość od klienta.
  4. Generuje odpowiedz i ja wysyła. Potem znowu nasłuchuje wiadomości od klienta lub końca połączenia.
  5. Serwer oczekuje przy odbiorze tylko bajów wiadomości i sam wysyła tylko bajty wiadomości-odpowiedzi.

Zadanie

Jak wydajnie/sprytnie/prosto zaimplementować cześć kliencka? Chodzi o takie rozwiązaniem, które niekoniecznie będzie jak najbardziej niskopoziomowe, lecz proste i łatwo utrzymywane. Można użyć Socket czy TcpClient. Niech w tym rozwiązaniu szybkość wymiany informacji raczej będzie limitowana ograniczeniami fizycznymi sieci niż jakimś skomplikowanym przetwarzaniem informacji z sieci przez klienta.

Uwagi

  1. Implementacja serwera jest stała i nie można jej zmieniać.
  2. Przykładowa implementacja serwera może być taka ze generuje losowa wiadomość jako odpowiedz o długości od 200 B do 8 kB.
  3. Należy oczekiwać ze czasami (powiedzmy 10%) sytuacji będą opóźnienia na sieci kilkunastu/kilkudziesięciu milisekundowe, czasami parę sekund.
  4. Pytania proszę zadawać w komentarzach.
Podręczne szyfrowanie danych w .NET

Ten tekst powstał w nawiązaniu do artykułu Michała Grzegorzewskiego [Zabawy z LSA - wydłubywanie haseł usług] oraz Jego nawoływaniu do napisania czegoś o ProtectedData i ProtectedMemory.

ProtectedData

Wyobraźmy sobie scenariusz gdzie posiadamy ekran logowania wraz z opcją zapamiętania hasła użytkownika. Opcja zapamiętywania służy temu głównie, aby użytkownikowi opcjonalnie sugerować jego hasło gdy działa głównie na jednej stacji roboczej. Zazwyczaj przecież jest tak, że pracujemy w domu czy pracy na jednej maszynie która jest nawet przypisana do nas jako część stanowiska pracy.

Akt 1

Przykład ten ilustruje projekt ZineNet.DataProtection.Act1. Podczas uruchamiania aplikacji użytkownik wpisuje nazwę użytkownika oraz hasło i zaznacza/odznacza opcję "zapamiętaj hasło". Hasło zapisywane jest w jeden z typowych sposobów dla aplikacji .NET, czyli ustawieniach użytkownika Properties/Settings.settings. Przy kolejnej autentykacji hasło jest już wpisane i tylko użytkownik wpisuje nazwę i wciska "Enter". Co za user experience! Przecież nikt nie lubi zapamiętywać haseł!

Zajrzyjmy teraz co stało się z zapamiętaną wartością hasła. Plik konfiguracji uzytkownika możemy w naszym przypadku znaleźć w katalogu profilu użytkownika, coś mniej więcej:
Local Settings\Application Data\ZineNet\ZineNet.DataProtection.cos_tam\1.0.1.0\user.config. Zaglądamy do środka i tu rozczarowujący widok. Hasło przechowywane jest w czystej postaci, jak go użytkownik stworzył.

<configuration>
...
  <userSettings>
    <ZineNet.DataProtection.Act1.Properties.Settings>
      <setting name="Logon_Password" serializeAs="String">
        <value>bbb</value>
      </setting>
    </ZineNet.DataProtection.Act1.Properties.Settings>
  </userSettings>
</configuration>

Akt 2

Taki sposób to dość marne zabezpieczenie, więc postaramy się teraz to naprawić. Najpierw zmieniamy typ zapamiętywanego hasła z System.String na System.Byte[] (i najlepiej nazwę wpisu ustawień użytkownika na wypadek kolizji i problemów z wersjonowaniem naszej aplikacji). W kolejnym kroku zamiast zapisywać hasło w czystej postaci jak poniżej:

if (_rememberPassword) {
  Properties.Settings.Default.Logon_Password = password;
  Properties.Settings.Default.Save();
}

Zapiszemy ją jako zaszyfrowany ciąg bajtów. Szyfrowanie możemy uzyskać za pomocą metody "ProtectedData.Protect(...)". Klasa ProtectedData ta faktycznie opakowuje funkcje systemowe DPAPI: CryptProtectData i CryptUnprotectData. Metoda Protect przyjmuje 3 parametry i zwraca ciąg bajtów, którym jest zaszyfrowany ciąg bajtów wejściowych.

Pierwszym parametrem jest "userData". Jest to ciąg bajtów dowolnej długości, który należy zaszyfrować.

Drugim parametrem jest "optionalEntropy". Jest to kolejny ciąg bajtów (może być innego rozmiaru niż userData) który zawiera losowe wartości wzmacniające szyfrowane dane. Idea jest taka sama jak z szyfrowaniem haseł i tak zwanym "ziarnem" (http://en.wikipedia.org/wiki/Salt_(cryptography)).

Trzeci parametr "scope" definiuje na jakim poziomie dane powinny być zabezpieczone. Możemy wybrać opcję szyfrowania w ramach konta użytkownika (wtedy tylko w kontekście tego samego użytkownika będzie można te dane odszyfrować) lub w ramach całej maszyny.

Poziom szyfrowania "maszyna" może być użyteczny na przykład to przechowywania haseł do serwera bazy danych, jeśli ten sam serwer jest używany przez wielu użytkowników z tej samej maszyny. Po prostu zaszyfrowane dane będzie mógł odszyfrować każdy, kto zaloguje się poprawnie do komputera.

W naszym przypadku pomijam opcjonalna wartość entropii i ustawiam poziom "użytkownika". Dzięki temu na tej samej maszynie różne osoby (konta) będą mogły używać naszej aplikacji nie przeszkadzając sobie.

Drugim krokiem jest zmiana uzyskiwania hasła już zapisanego dla kolejnych uruchomień aplikacji. Zamiast prostego:

_password = Properties.Settings.Default.Logon_Password ?? "";

Musimy użyć metody "ProtectedData.Unprotect(...)" w następujący sposób:

var encPassword = Properties.Settings.Default.Logon_PasswordEnc;
if (encPassword != null) {
  _password = Encoding.Unicode.GetString(ProtectedData.Unprotect(encPassword, null, DataProtectionScope.CurrentUser));
} else {
  _password = "";
}

Metoda Unprotect przyjmuje 3 parametry jak metoda Protect, z tym wyjątkiem że pierwszy parametr jest zaszyfrowanym ciągiem bajtów uzyskanym z wcześniejszego użycia metody Protect. Jeśli używamy opcjonalnej entropii, to musimy jako drugi parametr przekazać tą samą wartość której użyliśmy przy metodzie Protect. Poniższy pseudo kod ilustruje wymaganą zależność:

var data = ...
var entropy = ...
var scope = ...

var enc = Protect(data, entropy, scope
var data2 = Unprotect(enc, entropy, scope)

assert(data == data2)

Zaglądamy ponownie do pliku user.config tym razem w katalogu Local Settings\Application Data\ZineNet\ZineNet.DataProtection.cos_tam\1.0.2.0\user.config i widzimy tym razem coś o wiele porządniejszego:

<configuration>
...
  <userSettings>
    <ZineNet.DataProtection.Act2.Properties.Settings>
      <setting name="Logon_PasswordEnc" serializeAs="Xml">
        <value>
          <base64Binary>AQAAANCMnd8BFdERjHoAwE...</base64Binary>
        </value>
      </setting>
    </ZineNet.DataProtection.Act2.Properties.Settings>
  </userSettings>
</configuration>

Gotową aplikację, a dokładniej jej projekt można znaleźć w załączonych plikach do artykułu pod nazwą ZineNet.DataProtection.Act2

ProtectedMemory

Podobne działanie jak ProtectedData mają metody klasy ProtectedMemory. Są trzy zasadnicze różnice.

Pierwszą jest to że metody ProtectedMemory opakowują metody DPAPI CryptProtectMemory i CryptUnprotectMemory i jako takie nie przyjmują już opcjonalnej wartości entropii.

Drugą różnicą jest inny typ enumeracji dla parametru scope. Możemy wybrać opcję szyfrowania wewnątrz procesową, między procesową oraz w ramach kontekstu użytkownika.

Trzecią różnicą jest to, że możemy szyfrować i deszyfrować w ramach jednego uruchomienia maszyny. Restart komputera wymazuje informacje o tym jak zdeszyfrować poprzednio szyfrowane dane. Typowym zastosowaniem jest szyfrowanie tych danych, które są ścisłe sesyjne, czy muszą istnieć tylko podczas działania aplikacji i nie są nigdzie zapisywane.

Zagadka 1 - Jak to jest możliwe?

Przegladajac poranna dawke postow na blogach (nieodlaczny Google Reader), natknalem sie na ciekawy i zaskakujacy fragmetn kodu. Zaczalem doglebniej przegladac siec i swoje zasoby dyskowe i moje zaskoczenie roslo jeszcze bardziej. Dosc gadaniny zapraszam do rozwiazania zagadki (raczej nie przewiduje nagrod). Niestety ta zagadka ma jeden feler (ktory sam nie znosze) - dobra odpowiedzia jest ta ktora mam na mysli. Czyli jest to raczej proba zainteresowania ciekawym zjawiskiem jezyke C# niz prawdziwa zagadka.

Ponizej jest fragment kody C#. Z pewnego typu wycialem kod konstruktora i pola. Pytanie: Jak to jest mozliwe?

public static readonly Aaa Empty;
public Aaa(int x, int y) {
    this = Empty;
}

"Jak" znaczy wskazac odpowiedni fragment specyfikacji języka C#.

EDIT: Poprawna odpowiedz dal "apl". Gratulacje!

public struct MyStruct {
    public static readonly MyStruct Empty;
    private int _x;
    public MyStruct(bool notUsed) {
        this = Empty;
    }
}

Warto w tym momencie przytoczyc fragment dokumentacji C# 3 dotyczacy slowa kluczowego this w kontekscie klasy i struktury:

Klasa:

"When this is used in a primary-expression within an instance constructor of a class , it is classified as a value. The type of the value is the instance type (§10.3.1) of the class within which the usage occurs, and the value is a reference to the object being constructed."

Struktura:

"When this is used in a primary-expression within an instance constructor of a struct, it is classified as a variable. The type of the variable is the instance type (§10.3.1) of the struct within which the usage occurs, and the variable represents the struct being constructed. The this variable of an instance constructor of a struct behaves exactly the same as an out parameter of the struct type—in particular, this means that the variable must be definitely assigned in every execution path of the instance constructor."

Z pierwszego zdania w obu tekstach widac ze sa traktowane inaczej w klasie i strukturze. Dodatkowo z tego co z rozumialme z reszty wyjasnienia dla struktury, to "value" nalezy traktowac jak parameter out (nalezy zainicjalizowac out-parametr przed opuszczeniem metody), czyli cos w rodzaju:

public struct MyStruct {
    public MyStruct(out MyStruct this) {
        ...
    }
}

Lecz porownujac prawdziwy parametr "out" i ten pseudo, wychodza na jaw znaczace roznice:

public struct MyStruct {
    private int _x;
    public MyStruct(/* out int this, */ int x) {
        this._x = x;
    }
}
public class MyClass {
    private int _x;
    public void MyClassMethod(out MyClass @this, int x) {
        @this._x = x;
    }
}

W drugim przypadku dostaniemy blad kompilacji "error CS0269: Use of unassigned out parameter 'this'" dla linii "@this._x = x;".

Podgladajac kod IL wygenerowany dla roznych przypadkow, dochodze do wniosku, ze konstrukcja "this =" sluzy poprostu za inicjalizator struktury zap omoca wartosci po prawej stronie znaku rownosci konstrukcji "this =". Inicjalizator w sensie "memcpy" - czyli tak jak dziala przypisanie struktur jezyka C# - kopiujac wartosca nie przypisujac referencje.

Podsumowujac, przegladajac blogi i czytajac kod (w tym przypadku MEF Preview 2), czasami mozna czegos nowego sie nauczyc ( a dokumentacje C# poprawic ;-) ).

BTW: Wyglada jak by w .NET 4, proste pojemniki na dane (takie jak opisywalem w jednym z tekstow) beda dostepne "z pudelka"!

[Ww.Misc] Resharper 4.1 i dalej

Zapewne dzis (2008-08-29) lub najdalej w poniedzialek wyjdzie wersja 4.1 (4.0.1) swietnego narzedzia Resharper. Nie jest to oficjalna wiadomosc, jako ze nie ma tego do sciagniecia czy na oficjalnych blogach, lecz bugi dla wersji 4.1 zostaly oznaczone jako "Release Date: 29 Aug 08" (http://www.jetbrains.net/jira/browse/RSRP/fixforversion/11620), od dluzszego casu coraz rzadziej sa publikowne nowe buildy no i mozna zauwazyc ze 4.5 pojawilo sie w ichniej "bugzilli". Ponizej pare luznych uawg, mysli na temat R#.

Narzedzie

Ogolnie jest to najlepsze narzedzie wspierajace VS z jakim moglem pracowac. Naprawde w codzeinnej pracy pomaga mi znacznie. Do mniej "slodkich" uwag dodalbym wysokie wymagania pamieciowe i sporo trzeba poswiecic na konfiguracje aby uzywanie R# bylo intuicyjne (co nie znaczy nie mozliwe). Cos za cos - ja nadal uwazam ze wady sa calkowiecie zrekompensowane przez zalety tego narzedzia

Support

Tu, juz nie jest tak rozowo. Jedynym widocznym plusem jest automatyczne raportowanie bledow po wystapienu. W czasach wersji 1.x czy 2 druzyna R# byla bardziej sklonna uznawac zgloszone uwagi/problemy jako bugi. Niestety podzcas ostatnich wersji, pod uwage brane sa uwagi krytyczne (wykladanie VS, czy blokowanie VS), pozostale zazwyczaj koncza w szufladce "w nastepnej wersji" albo "won't fix".

Release

Wersja 4.x produkowana byla naprawde  dlugo! Prezkroczono obiecywane terminy 2-3 krotnie. Instalowalem "Nightly Builds" w miare regularnie (pewnie tak >80% opublikowanych buildow zainstalowalem) i w miare regularnie przegladalem stan "buzgilli" tego projektu. Zauwazylem jedna ciekawa rzecz. A mianowicie przed opublikowaniem wersji 4.0 mieli kilka(nascie) blockerow i kilkadzeisiat criticali (chyba bylo kolo 80 albo odrobine ponad 100 - dokladnie nie pamietam), lecz mimo to produkt wypuscili! Zapewne niezadoloenie w "comunity" z niewypuszczanie RTM mialo swoj udzial w szybkiej publikacji, lecz chyba troche zbyt pochopnie. Zadziwiajaca rzecz stala sie pare dni po publikacji - bugi zostaly przerzucone do wersji 4.0.1 (chyba to sie nazywalo jeszcze wersja 4.0.1, bo ostatnio ma to nazwe 4.1). Wywnioskowac z tego mozna ze wersja 4.0 byla uznana arbitralnie (aby zatkac usta niezadolonym) a prace trwaly nadal.

Zadziwiajaca sytuacje obserwuje ponownie. Pojawi sie wersja 4.1 i juz bugi sa przerzucane do 4.5 lub dalej. W tej chwili zdecydowana wiekszosc bugow przerzucili do 4.5. Chyba maja skrypt jakis w stylu "Dodaj nowa wersje i przerzuc wszystkie zgnilki.cmd" ;-)

Szczotka + Zmiotka

Nie podoba mi sie sytuacje, gdzie w wersji X zgloszony jest problem (nie jeden i nie dwa!) klasyfikowany jako powazny i nic sie z nim nie dzieje przez kolejnych kilka wersji. Sukcesywnie jest przerzucany z wersji do wersji. Uwazam ze powinni sie zdecydowac na "won't fix" (bo taka maja wizje produktu), badz rozwiazac go bez nieskonczonego przesuwania. A'propo takiego zwodzenia, przypomina mi sie sytuacja gdzie kupilem licencje na ktoras wersji, powodowany tym ze rozwiazany zostanie pewien powazny dla mnie problem. mial byc rozwiazany tuz po pierwszym oficjalnym release i nawet tak byl oznaczony w "bugzilli". Niestaty pomimo tego oznaczenia i zapewnien ze strony "kogos w supporcie", to sie nie stalo. Krotko mowiac zostalem oszukany. Jakos mam wrazenie ze podobnie dzieje sie teraz z tym przesuwaniem bugow z wersji na wersje...

Wersja 4.5

Pomimo tych wszelkich "dziwactw" teamu R#, ciekaw jestem jakie nowosci zaproponuja w nowej, ulamkowej wersji. Trudno cos zgadywac, jako ze aktualnie jest dosc pelnym produktem. Moze ktos z Was ma jakis pomys/przepowiednie?

...

Pomimo ze nie podoba mi sie sposob dzialania Supportu R#, na pewno nastepna wersje bede uzywal, lecz przy kolejnym uaktualnieniu dobrze sie zastanowie. Kto wie, moze jakies nowe narzedzie sie pojawi...

opublikowano 29 sierpnia 08 12:18 przez Wojciech Gebczyk | 13 komentarzy   
Zarejestrowano w kategorii: ,
[Ww.Note] Male przyspieszacze czasu

Chcialbym sie podzielic malymi fragmentami kodu, bedacymi przspieszaczami czasu. Podobny tekst opublikowal Maciej Aniserowicz. Przykłady dotycza generowania obiektow string z roznego rodzaju powtorzeniami oraz prawie powtorzeniami.

1. Wygenerowanie tekstu ze zbioru napisow poprzedzielanych jakims delimiterem.

{
    var arr = new[] { "ala", "ma", "kota" };
    var s = String.Join(" ", arr);
    // s == "ala ma kota"
}
{
    var arr = new[] { "'a'", "'b'", "'c'" };
    var s = "Foo(" + String.Join(", ", arr) + ")";
    // s == "Foo('a', 'b', 'c')"
}
{
    var arr = new[] { "arg1", "arg2", "arg3" };
    var s = "Foo(" + String.Join(", ", arr) + ")";
    // s == "Foo(arg1, arg2, arg3)"
}

Metoda String.Join spelnia znakomicie swoja role.

2. Wygenerowanie tekstu gdzie mamy sekwencje takich samych wartosci, poprzedzielanych jakims inneym tekstem (przecinek, tabulator, nowa linia)

var s = String.Join("; ", Enumerable.Repeat(DateTime.Today.ToString("yyyy-MM-dd"), 5).ToArray());
// s == "2008-08-22; 2008-08-22; 2008-08-22; 2008-08-22; 2008-08-22"

Ponownie metoda String.Join, lecz tym razem tablice generujemy pomocnicza konstrukcja Enumerable.Repeat

3. Wygenerowanie tekstu gdzie mamy sekwencje wartosci, poprzedzielanych jakims inneym tekstem

var s = "Foo(" + String.Join(", ", Enumerable.Range(1, 4).Select(i => "arg" + i).ToArray()) + ")";
// s == "Foo(arg1, arg2, arg3, arg4)"

Ponownie metoda String.Join, lecz tym razem tablice generujemy pomocnicza konstrukcja Enumerable.Range razem z Select.

W starych czasach (pewnie w okolicach zdobywania Berlina) stosowalem StringBuildera i flagi LUB generowalem tablice za pomoca for i uzywalem String.Join. Oczywiscie nie zawsze takie konstrukcje sie nadaja, zwlaszcza ze w niektorych przypadkach performance (CPU, memory) moze cierpiec. Ale jesli uzywamy tego z umiarem czy w szablonach T4 (stad wzial sie pomysl na ten tekst), to jest wedlug mnie jak najbardziej na miejscu.

opublikowano 22 sierpnia 08 01:14 przez Wojciech Gebczyk | 7 komentarzy   
Zarejestrowano w kategorii: , , , , ,
[Ww.VS] Szablony tekstowe w VS2008

Od chwili pojawienia się DSL Tools dla Visual Studio, dostępne były szablony tekstowe T4. Pisał o nich Arek w drugim Zine. Wraz z Visual Studio 2008, bez instalowania SDK czy DSL Tools, szablony T4 są dostepne "z pudełka". Zreszta ostatni wpis na blogu Arka (ten w ktorym używa takich skomplikowanych zwrotów jak refaktoryzacja czy metoda zwrotna kojarząca mi się z kolejami).

Jak najszybciej użyć T4?

Poprostu należy dodać plik o rozszerzeniu tt do projektu i VS2008 automatycznie podłączy CustomTool "TextTemplatingFileGenerator" i wygeneruje podłączony plik cs.

Wystarczy teraz w pliku napisać cos takiego:

namespace MyApp {
  class Class1 {
<# foreach (string name in new string[] { "Aaa", "Bbb", "Ccc" }) { #>
   string <#= name #> { get; set; }
<# } #>
  }
}

Aby otrzymać taki wynik:

namespace MyApp {
  class Class1 {
   string Aaa { get; set; }
   string Bbb { get; set; }
   string Ccc { get; set; }
  }
}

Parę uwag przy dodawaniu szablonów T4

  1. Za pierwszym razem możemy dostać ostrzerzenie "Security Warning" informujące o niebezpieczeństwie zniszczenia komputera pewnie razem z księżycem. Najlepiej potwierdzić za pomocą OK, zaznaczając wczesniej CheckBox "Do not show this message again".
  2. Plik tekstowy bez dyrektyw szablonu (znaczników na początku pliku <#@ i #>) czy znaczników renderowania, również jest poprawnym szablonem, który generuje tekst identyczny z zawartościa szablonu
  3. Wygodnie jest dodac nowy szablon za pomocą opcji "Add>New Item...", wybrać dowolny tekstowy plik jak Class czy Code File zmieniając rozszerzenie pliku na ".tt" zamiast ".cs"
  4. Standardowo plik generowany pod szablonem ma rozszerzenie ".cs" (to w przypadku projektów C#, zapewne w przypadku projektów VB będzie to ".vb" - nie sprawdzałem). Wiąże się to również z tym że zachowanie pliku w projekcie będzie takie jak typowego pliku o tym rozszerzeniu - w przypadku plików ".cs" "Build Action" będzie ustawiony na "Compile".
  5. Jesli wybierzemy inny plik (na przykład XML File), to standardowo dostaniemy wygenerowany plik ".cs". Generowane rozszerzenie można zmienić dodając dyrektywę output na przykład tak: <#@ output extension="xml" #> - koniecznie na początku pliku!
  6. Jesli plik szablonu o rozszerzeniu ".tt" skopiowany do schowka (na przykład z pulpitu, czy Exploratora Windows) wkleimy do projektu, to również poprawnie zarejestruje nam sie szablon.
  7. Jesli istniejącemu plikowi (na przykład ".cs") zmienimy rozszerzenie na ".tt", to znowu otrzymamy działający plik szablonu. Jedyna niedogodnościa może być to że wygenerowany plik pod szablonem będzie miał w nazwie suffix "1" (lub inna cyfrę). Można tego uniknąć stosując inną metodę, lub ręcznie edytując plik projektu ".csproj" (Unload Project a potem Edit) i usuwając element "LastGenOutput" z elementu odpowiadającego szablonowi. Po ponownym załadowaniu należy usunąć poprzednio wgenerowany plik i wymusić generację ponownie (opcja Run Custom Tool).

Mały bonus

W archiwum ZIP, znajdują się 4 pliki szablonów. Wystarczy je rozpakować, skopiować i wkleić do projektu, aby otrzymać parę klas "utilsowych".

Guard i GuardDebug - to klasy podobne do Assert, lecz posiadające API bazujące na xUnit.

Entries - to zbiór prostych klas-pojemników, do zastosowań wewnątrz implementacyjnych (nie zalecane "wystawianie" tych typów w publicznym API).

Przykładowe użycie:

Guard.NotNull(param1, "Param1 cannot be null.");
GuardDebug.NotNullOrEmpty(name, "Entity name cannot be empty.");
var e = new Entry<int, int, int>(1, 2, 3);
Guard.InRange(e.Value2, e.Value1, e.Value3, "Entry constraint not satisfied.");
var map = new Dictionary<string, Entry<string, int, int>>();

Jeśli macie jakieś uwagi zapraszam do komentowania lub kontaktu mailowego.

Oczywiscie standardowy disclaimer ze nei odpowiadam za problemy, wybuchy etc.

opublikowano 21 lipca 08 09:43 przez Wojciech Gebczyk | 12 komentarzy   
Zarejestrowano w kategorii: , , , , , ,
[Ww.Text] CustomConfig2 - czyli jak zrobic aby sie nie narobic

Wczoraj przeczytaLem post Darka omawiający hierarchiczne pliki konfiguracyjne. Podczas czytania naszły mnie 3 mysli.

  1. Kropkowana notacja przypomina mi Javowe properties (czy to jescze tak sie nazywa?). Mialem z tym doczynnienia daawno temu jak jeszcze cos robilem przy Java. Srednio mi sie to posdobalo i do dzis uwazam ze reprezentacja hierarchii wartosci za pomoca notacji kropkowej a nie skorzystanie z gotowego, znanego, popularnego, sprawdzonego rozwiazania nie jest najszczesliwszym rozwiazaniem (do typowych zastosowan, jako ze na pewno znajda sie zastosowanie gdzie take java-ini-like rozwiazanie sprawdzi sie duzo lepiej niz XML).
  2. Po moich ostatnich "zawodowych" walkach z przetwarzaniem plikow tekstowych i problemami stron kodowych, unicode, kompatybilnosci wstecz i wprzod (na szczescie bez kompatybilnosci gora-dol czy prawo-lewo ;-) ) ustawiem regionalnych, jezyka systemu i non-win systemow - cisnienie mi skoczylo na mysl o sledzeniu problemow i ich rozwiazywaniu. Po pierwszym tekscie nie widac na razie jak tego typu problemy moga byc rozwiazane. Prawdopodobnie dlatego ze Darek nie wymaga podobnej funkcjonalnosci od tego rozwiazania i jemu to wystarcza to jest calkowicie w porzadku.
  3. Pozazdroscilem checi pisania, testowania, dokumentowania, wdrazania tego rozwiazania w zespole, jakoze jestem leniwa osoba. Z tego tez powodu powstal ten blog, poniewaz postanowilem zrobic podobna funkcjonalnosc korzystajac z wbudowanej funkcjonalnosci w .NET Frameworka.

Configuration w .NET Frameworku wspiera chyba od poczatku hierarchiczne pliki konfiguracyjne. Przeciez po instalacji mamy machine.config i "app.config". Ten drugi moze zmienic "odziedziczone" wlasnosci z machine.config lub dodac nowe a nawet usunac - oczywiscie wszystko "lokalnie" w ramach aplikacji. Wiec wykorzystajmy ta infrastrukture do "naszych" celow - czyli rozwiazania ktore posiada glowny plik konfiguracyjny oraz opcjonalne per user nadpisane ustawienia.

Nie stadardowy plik konfiguracyjny

Najpierw pokaze jak zaladowac konfiguracje z prawie dowolnego pliku. korzystajac z metody ConfigurationManager.OpenExeConfiguration(string exePath) mozemy zaladowac plik konfiguracyjny dla wskazanego pliku "exe" (a tak naprawde dowolnego pliku) gdzie plik konfiguracyjny to exePath z dodanym sufiksem ".config".



const string cfg = @"<?xml version='1.0' encoding='utf-8' ?>
    <configuration>
      <configSections>
        <section name='mySection' type='ZineNet.CustomCfg2.MySection, ZineNet.CustomCfg2'/>
      </configSections>
      <mySection>
        <myProperties>
          <add name='p1' value='v1' />
          <add name='p2' value='v2' />
          <add name='p4' value='v4' />
        </myProperties>
      </mySection>
    </configuration>"
;
File.WriteAllText(@"CfgDir\Demo1.tmp", "");
File.WriteAllText(@"CfgDir\Demo1.tmp.config", cfg, Encoding.UTF8);

var exeCfg = ConfigurationManager.OpenExeConfiguration(Path.GetFullPath(@"CfgDir\Demo1.tmp"));
var myCfg = (MySection)exeCfg.GetSection("mySection");

foreach (MyPropertyElement p in myCfg.MyProperties) {
    Console.WriteLine("{0}:{1}", p.Name, p.Value);
}

W tym przykladzie tworze plik na dysku, gdze zapisuje przykladowa konfiguracje. Nastepnie otwieram plik konfiguracyjny wskazujac na tymczasowy/sztuczny plik CfgDir\Demo1.tmp. Metoda OpenExeConfiguration sprawdza istnienie pliku przekazanego w argumencie exePath, pomimo iz z niech nie korzysta. Dlatego wytwarzam rowniez pusty plik o takiej nazwie.

Dowolne hierarchiczne pliki konfiguracyjne

Teraz przyklad blizszy temu co zaprezentowal Darek w swoim tekscie. Utworze 2 pliki konfiguracyjne, zaladuje oba uzyskujac ten sam typ sekcji konfiguracyjnej jak w przykladzie pierwszym.



const string cfg = @"<?xml version='1.0' encoding='utf-8' ?>
    <configuration>
      <configSections>
        <section name='mySection' type='ZineNet.CustomCfg2.MySection, ZineNet.CustomCfg2' allowExeDefinition='MachineToRoamingUser'/>
      </configSections>
      <mySection>
        <myProperties>
          <add name='p1' value='v1' />
          <add name='p2' value='v2' />
          <add name='p4' value='v4' />
        </myProperties>
      </mySection>
    </configuration>"
;
const string cfgLocal = @"<?xml version='1.0' encoding='utf-8' ?>
    <configuration>
      <mySection>
        <myProperties>
          <remove name='p2' />
          <add name='p5' value='v5' />
        </myProperties>
      </mySection>
    </configuration>"
;
File.WriteAllText(@"CfgDir\Demo2.config", cfg, Encoding.UTF8);
File.WriteAllText(@"CfgDir\Demo2.Local.config", cfgLocal, Encoding.UTF8);

var exeCfg = ConfigurationManager.OpenMappedExeConfiguration(
    new ExeConfigurationFileMap {
        ExeConfigFilename = Path.GetFullPath(@"CfgDir\Demo2.config"),
        RoamingUserConfigFilename = Path.GetFullPath(@"CfgDir\Demo2.Local.config")
    },
    ConfigurationUserLevel.PerUserRoaming
);
var myCfg = (MySection)exeCfg.GetSection("mySection");

foreach (MyPropertyElement p in myCfg.MyProperties) {
    Console.WriteLine("{0}:{1}", p.Name, p.Value);
}

Tym razem uzywam innej metody OpenMappedExeConfiguration gdzie przekazuje obkekt ExeConfigurationFileMap. Obiekt file map zawiera sciezki do konkretnych plikow konfiguracyjnych a nie zaleznych plikow "exe". Za pomoca drugiego argumentu ConfigurationUserLevel specyfikujemy ktore pliki musza byc zaladowane, a ktore zignorowane, jako ze w typie ExeConfigurationFileMap mozeny wyspecyfikowac 4 pliki konfiguracyjne roznego poziomu. Musimy zezwolic na ta operacje przy definicji sekcji w "pierwszym" pliku konfiguracyjnym za pomoca atrybutu "allowExeDefinition".

Jeszcze wiecej funkcjonalnosci we wlasnych plikach

Powyzszy przyklad dostarcza z grubsza funkcjonalnosci zaprezentowanej przez Darka. Lecz mozemy skorzystac bez zmiany kodu przetwarzajacego, z pozostalej funkcjonalnosci. Dla przykladu pokaze mozliwosc blokowania redefiniowania fragmentow konfiguracji.

const string cfg = @"<?xml version='1.0' encoding='utf-8' ?>
    <configuration>
      <configSections>
        <section name='mySection' type='ZineNet.CustomCfg2.MySection, ZineNet.CustomCfg2' allowExeDefinition='MachineToRoamingUser'/>
      </configSections>
      <mySection>
        <myProperties>
          <add name='p1' value='v1' />
          <add name='p2' value='v2' lockItem='true' />
          <add name='p4' value='v4' />
        </myProperties>
      </mySection>
    </configuration>"
;
const string cfgLocal = @"<?xml version='1.0' encoding='utf-8' ?>
    <configuration>
      <mySection>
        <myProperties>
          <remove name='p2' />
        </myProperties>
      </mySection>
    </configuration>"
;
File.WriteAllText(@"CfgDir\Demo3.config", cfg, Encoding.UTF8);
File.WriteAllText(@"CfgDir\Demo3.Local.config", cfgLocal, Encoding.UTF8);

var exeCfg = ConfigurationManager.OpenMappedExeConfiguration(
    new ExeConfigurationFileMap {
        ExeConfigFilename = Path.GetFullPath(@"CfgDir\Demo3.config"),
        RoamingUserConfigFilename = Path.GetFullPath(@"CfgDir\Demo3.Local.config")
    },
    ConfigurationUserLevel.PerUserRoaming
);
var myCfg = (MySection)exeCfg.GetSection("mySection");

foreach (MyPropertyElement p in myCfg.MyProperties) {
    Console.WriteLine("{0}:{1}", p.Name, p.Value);
}

Ustawiajac atrybut lockItem='true' na jednej z wlasnych wlasciwosci, nie pozwalamy na redefiniowanie go w "potomnym" pliku konfiguracyjnym. Podczas uruchamiania przykladu trzeciego dostaniemy wyjatek o podobnej tresci: "The attribute 'p2' has been locked in a higher level configuration. (K:\ZineNet\ZineNet.CustomCfg2\bin\Debug\CfgDir\Demo3.Local.config line 5)".

Inne uwagi

  1. Zamiast hierarchii zagniezdzania elementow, mozna ja "rozplaszczyc" i zastosowac notacje kropkowa podobnie jak to uczynil Darek. Poprostu trzeba nadpisac w definicji sekcji sposob deserializacji/serializacji
  2. Podejscie z zagniezdzaniem elementow XML zamiast notacji kropkowej wymusza na nas stworzenie odpowiednich klas konfiguracji. Zyskujemy na tym walidacje pliku konfiguracyjnegom lecz tracimy na tym ze mamy dodatkowa prace rzemieslnicza zwiazana z ich napisaniem.

 

opublikowano 12 czerwca 08 10:56 przez Wojciech Gebczyk | 0 komentarzy   
Zarejestrowano w kategorii: , , , , ,
[WG.net] Wzorzec Model View Controller (MVC)

Prezentacja z XX spotkania grupy WG.net (XI PGU SqlSever) omawiająca wzorzec MVC.

Scenariusz problemu - Edycja danych

Jednym z najczęstszych przypadków tworzenia aplikacji jest umożliwienie użytkownikowi interakcji z danymi - formatkowy edytor danych. Zakres wymaganej funkcjonalności obejmuje bardzo często następujące wymagania:

  • Zapytanie do bazy - wszystkie / tylko wybrane
  • Przepisanie danych do kontrolek - problemy z DBNull, null, etc
  • Obsługa słowników
  • Obsługa akcji Edit/Add/Remove/BrowseDictionary
  • Sprawdzenie poprawności
  • INSERT/UPDATE/DELETE do bazy

Implementacja całości wymagań w jednym pliku asp/php/form etc powoduje nieczytelność kodu. Przeglądanie, a co ważniejsze koszt utrzymywania kodu rośnie coraz szybciej wraz ze wzrostem dodatkowej funkcjonalności.

Rozwiązanie - wzorzec MVC

Rozwiązaniem problemu jest zastosowanie wzorca Model View Controller (MVC). Polega to na podzieleniu wymaganej funkcjonalności/odpowiedzialności na 3 komponenty:

  • Model – przetwarzane dane
  • View – prezentacja danych
  • Controller – sterowanie przepływem danych – logika aplikacji

Dokładny podział odpowiedzialności jest zależny od konkretnego wariantu wzorca czy implementacji. Dla przykładu rozważmy odpowiedzialność "sprawdzenie poprawności danych". Można włączyć ją do modelu gdy nie jest ona trywialna, a powiązana ściśle z modelem. Walidację można włączyć do widoku, gdzie poszczególne kontrolki mogą ją wykonywać. W przypadku skomplikowanego sprawdzania odpowiedzialność może przejąć kontroler.

Warianty

Wzorzec MVC posiada wiele wariantów, dostosowywanych do konkretnych technologii, metodyk programowania czy właściwości języka. Zanim zacznie się tworzenie własnej implementacji, warto przejrzeć najpopularniejsze Frameworki MVC przeznaczone dla swojej platformy programistycznej. Zazwyczaj takie gotowce zawierają dodatkową funkcjonalność jak pojemniki IoC, definiowanie przepływem logiki aplikacji, ułatwienia typowych operacji programisty, etc.

[Prezentacja 20080214_XX_WG_WzorzecMVC.zip] oraz [Ww.WgNet.MvcCalc.zip]

opublikowano 16 lutego 08 03:29 przez Wojciech Gebczyk | 19 komentarzy   
Zarejestrowano w kategorii: , , ,
[Ww.Web] SilverLight, SilverDark, Konkrety

Poniżej parę konkretnych uwag do SilverLighta (SL), aby poprzednie narzekanie na SL nie było gołosłowne. Nie są to wszystkie moje uwago do SL, ale wydaje mi się wystarczająca ilość aby uzasadnić mój punkt widzenia z poprzedniego postu.

1. Środowiska programisty/projektanta/tego-co-robi-SL-aplikację

Spróbowałem pracować z Blendem oraz VS 2008. Niestety, kontrolki z Toolkita nie pokazują się w designerze i w dodatku z jakiegoś magicznego powodu po uruchomieniu wyglądają dziwnie (niektóre elementy są powyciągane, a niektóre mają dobre rozmiary). Zrezygnowałem z Blenda i zacząłem używać VS2008. Ręczna praca z XAMLem oraz kodem "pode spodem" przyniosła efekty - na statycznych plikach z tekstem do wyświetlenia działało dobrze! Niestety próba udynamicznienia aplikacji spowodowała stratę około 2 godzin (jeden wieczór zmarnowany), ponieważ w wersji 1.1 Downloader uruchomiony lokalnie (nie przez serwer Web) nie pozwala "ściągnąć" plików leżących po sąsiedzku ze strona HTML aplikacji SL. Błąd wyświetlany jest baaardzo ogólny i nie wiadomo czy przypadkiem to początkujący SLowiec nie popełnił błędu. Informację o tym znalazłem na forum dyskusyjnym.

2. Debugowanie aplikacji

Chciałem mieć 3 projekty: w pierwszym logika konwersji, w drugim kontrolki związane z SL, a w trzecim prosta aplikacja w ASP.NET zajmująca się publikowaniem Package pod wirtualnymi adresami URL. Tak naprawdę projekt SL i ASP.NET powinny być razem, jako że występują w tym samym Web Site. Niestety nie udało mi się dodać projektu z kontrolkami Toolkita do aplikacji ASP.NET. Wyjściem okazało się dołączenie projektu Toolkita do projektu SL, ręczne włożenie projektu SL do katalogu z projektem ASP.NET i zmiany w pliku solution. Aby debugować jednocześnie ASP.NET i SL należy ustawić oba projekty jako startowe. Dostajemy przy starcie 2 przeglądarki - jedna od ASP.NET (localhost), a druga do SL (local file system) i musimy ręcznie przeklejać i koordynować obie przeglądarki.

3. Uboga podstawowa funkcjonalność

Jak pisałem w poprzednim poście, są tylko naprawdę podstawowe kontrolki. API SL pod .NET jest bardzo zubożone - brak przeładowań metod dla typowych operacji, wygląda jak jakiś wrapper na coś z zupełnie innej bajki. Taka klasa Downloader - koszmarne API, wszystko "lata" jako tekst. XamlReader - tylko metoda Load z parametrem string reprezentującym tekst do sparsowania - nie można przekazać strumienia danych. XamlReader zachowuje się dodatkowo nie przewidywalnie - ten sam tekst przekazany do metody Load czasami przetwarza się poprawnie a czasami zgłasza błąd (Wygląda jak by po przetworzeniu określonej ilości znaków? elementów? zgłaszał błąd - problem z wewnętrznym buforem?).

4. Brak kontrolki TextBox

To tak ważna kontrolka, że brak jej jest jakimś nieporozumieniem lub pomyłką. Resztę na ten temat pominę milczeniem.

5. Brak funkcjonalności layoutowania kontrolek

Pozycjonowanie absolutne, a layoutowanie ręczne w kodzie. W przypadku zegarka to może i zda egzamin jak ma się kilka elementów bez interakcji użytkownika. Ale zastanówcie się ile trzeba spędzić czasu aby zrobic Grida ze stylami, szablonami? Dyskwalifikacja produktu.

6. Brak bindowania danych

Ręczne "wpisywanie danych" w kontrolkę (TextBlock/Glyphs). Takie "tricki" można było robić już w HTMLu i PHP/ASP. Po co do tego wracać? Dyskwalifikacja produktu.

7. Możliwości SL, a WPF

Gdzieś wyczytałem (jak zwykle nie pamiętam gdzie), że SL daje programiście duże możliwości, ponieważ jeśli ktoś chce to może zaimplementować na nim funkcjonalność WPF, a z tego wniosek, że to jest technologia porównywalna z WPF. Zgodzę się że technicznie jest możliwe przenieść sporą część funkcjonalności (nie napisze całość jako że nie znam tak dokładnie WPF), ale jakim kosztem czasowym?

Czasy gdy kilka lat temu walczyło się z materią, pisząc w HTML/JavaScript elementy z "dużych, okienkowych aplikacji" dla mnie przynajmniej już minęły. Nie cieszy mnie już sama możliwość zrobienia Czegoś bez względu na koszt czasowy. Odnosząc to do praktycznego zastosowania, jak widzę sposoby pracy i efekty SL:

a. SL jest przeznaczony dla aplikacji bazujących na HTML i przeglądarce. Aktualnie jest sens użycia tego jeśli klient zażyczył sobie JAWNIE aplikacji "webowej" z wodotryskami. Tak naprawdę jeśli klient chce intranetową, "bogatą" (bogatą w sensie UX/UI) aplikację są inne technologie zapewniające to, bez potrzeby debugowania JavaScript i udawania ze HTML jest prawie jak aplikacja okienkowa. Nie będę rozpisywał się o tym "prawie".

b. Jeśli już trzeba użyć HTML, to wtedy SL można użyć do prostych rzeczy, jak "flashowe" menu, ładne wykresy z odrobiną interakcji.

c. SL używamy do rzeczy prostych, ponieważ napisanie złożonej aplikacji (nawet formatowy edytor danych) wymaga naprawdę duuużo roboczo-godzin lub zasobów pieniężnych (na jedno wychodzi) . A to ze względu na potrzebę napisania tych kontrolek od początku, bądź kupna od jakiejś firmy. Z pobieżnego przeglądu dostępnych kontrolek jeszcze im daleko z funkcjonalnością choćby do podstawowych kontrolek WPFa.

[Ww.Web] SilverDark

Postanowiłem wziąć udział w konkursie Silverlighta (SL). Długo szukałem tematu. Po akademicku zacząłem w ostatniej chwili (czytaj: 2 dni temu). Poległem.

Czas pre-SL

WPFem zainteresowałem się gdzieś w początkach jego powstawania. Czekałem z zapoznaniem się z WPF tak naprawdę do czasu którejś z wersji Beta. Ciężka to była nauka, porównywalna chyba do nauki Flash 4. Do chwili obecnej napisałem 2-3 aplikacje, które są większe niż HelloWorld. Zderzyłem się z paroma problemami, zmieniłem sposób myślenia/oczekiwania co do pewnych komponentów WPF. Aktualnie uznaję WPF za dobrą alternatywę dla WinForms - może nie dla każdego zastosowania, lecz dla zdecydowanej ich większości. Potem pojawiły się informacje o WPF/E. Z nazwy oczekiwałem, iż będzie to taki WPF, ale na inne platformy.

"Lights, Camera, Action!"

Z oglądania kodu SL na blogach i w przykładach, wszystko wygląda fajnie, prosto i ładnie. Przykładowe aplikacje pokazują co można z tym zrobić. Myślałem, że zbieżność WPFa i SL będzie duża (w końcu pierwsza nazwa to WPF/E), i że za pomocą części wspólnej można tworzyć aplikacje WPF dobrze przenaszalne do SL. Och! Jaki ja bylem naiwny.

"Shileds up, weapons online"

Pojawia sie konkurs SL. Jest powód aby zapoznać się technologią. Najpierw wybór jaką aplikacje napiszę. Powstało wiele pomysłów: przeglądarka dokumentów (konwersja z DOCX,HTML,CHM,ect ->FlowDocument->XPS), dashboard z kilkoma elementami, smart tagi do HTMLa, itp. Zdecydowanym się w końcu na pierwszy pomysł - XPS reader. Pierwsze testy wykazały ze wystarczy z FixedPage wyrzucić kilka atrybutów i elementów i pozostanie część kompatybilna z XAMLem SL (okazało się że podobne podejście zastosował niejaki Delay). Potem zaczęły się kłopoty... Na przykład: jednym z ostatnich problemów jest niepoprawne parsowanie tekstu XAML. To znaczy ten sam tekst raz jest dobrze parsowany, a następnym razem zgłaszany jest wyjątek XamlParseException - co do znaku, co do bajtu ten sam tekst raz jest przetwarzany a raz nie.

Nastały mroczne czasy

Z końcu zrezygnowałem z udziału w konkursie. Na samą myśl o SL w tej chwili przychodzą mi na myśl w większości niecenzuralne określenia. Gdzieś, ktoś, kiedyś napisał ze SL to jeden ze sposobów na tworzenie aplikacji RIA - niestety według mnie w obecnym stanie wytworzenie nie trywialnej aplikacji wymaga samozaparcia i dużej ilości czasu.

Troche marudzenia

Dla osób które "programowały w HTMLu" budując za pomocą HTML/JavaScript niestandardowe elementy UI, spojrzenie na SL może wydawać się ożywczym zbawieniem! Większa szybkość, możliwości czy ujednolicenie API (żadnych CrossBrowser trików!) przyciąga. Ale na tym poziomie podobną funkcjonalność oferuje Flash (chyba? Niestety przestałem interesować się tą technologią gdzieś w okolicach wersji 6 lub 7). Zaletą SL wobec Flash jest prawdopodobnie brak naleciałości z poprzednich wersji jako ze jest to pierwsza wersja.

Z drugiej strony dla osoby która poznała najpierw WPF, a teraz próbuje użyć SL to coś z pogranicza koszmaru i komedii. Koszmarem jest samo programowanie, szukanie dokumentacji, debugowanie aplikacji i dochodzenie, czy problem leży po stronie programisty czy błędu w SL. Komedią jest literka R ze skrótu (RIA) - w wersji "z pudełka" mamy dostęp do 5 głównych kontrolek/elementów: Canvas, Image, TextBlock, Glyphs i Shape (wraz z pochodnymi). W Toolkicie są dodatkowe kontrolki, które niestety się "rozjeżdżają", mają nieintuicyjne API, ale przynajmniej są. Zgadam się, że do zrobienia sprawnie "aplikacji" zegarek, odtwarzacz filmików bądź "flashowe menu" w zupełności wystarczy. Aby zrobić cos bardziej użytecznego/zaawansowanego, trzeba włożyć w to dużo pracy.

Po tym pierwszym spotkaniu SL odkładam na półkę z rzeczami nie używanymi. Zresztą sam MS widzi potrzebę re ewaluacji tego produktu jako że 1.1 jest zawieszony i będzie kontynuowany jako 2.0 z poszerzonym zakresem funkcjonalności. Może wrócę do tego produktu, ale nie wcześniej niż udokumentowana, późna wersja Beta.

Konkurs Enterprise Library – Policy Injection (PIAB) - Updated

Oto ostatnia odsłona konkursu. Tym razem wprowadzenie do wykorzystania PIAB przy scalaniu poszczególnych rozwiązań opartych na LAB, EHAB i VAB.

Refaktoryzacja do Policy Injection Application Block (PIAB)

Jeżeli podczas projektowania aplikacji zdecydowaliśmy się na zastosowanie pewnych wytycznych czy standardów implementacji, wówczas spora część kodu wynikająca z tych ustaleń jest powtarzalna. Niestety, jeśli piszemy coś ręcznie istnieje możliwość popełnienia błędu. Aby uchronić się przed pomyłkami można napisać rozszerzenia lub makra Visual Studio pomagające nam we wdrażaniu standardów, niestety to zadanie nie należy do najłatwiejszych. Innym sposobem jest użycie narzędzia pozwalającego na zastosowanie elementów programowania aspektowego. Takimi aspektami mogą być na przykład logowanie, obsługa błędów czy walidacja. PIAB pozwala na dodawanie aspektów do nowych lub istniejących obiektów, dzięki wytwarzaniu obiektów proxy realizujących wymaganą funkcjonalność.

Przypadek

Walidacja, obsługa wyjątków oraz logowanie używają Application Block, lecz brak spójnych reguł ich użycia.

Rozwiązanie

Zlokalizuj fragmenty kodu z walidacją, logowaniem i obsługą wyjątków. Podziel poszczególne przypadki użycia, według sposobu przetwarzania i utwórz reguły ich dotyczące. Zaaplikuj reguły do istniejącego kodu.

Uzasadnienie

Co nam daje PIAB? Upraszcza kod aplikacji po przez automatyzowanie pewnych operacji w kodzie oraz zmniejsza ryzyko popełnienia błędu w przypadku ręcznego pisania kodu operacji.

Załóżmy, że celem logowania jest obsługa pewnych kroków scenariusza działania aplikacji – dla przykładu reguła mówi loguj każde wywołanie operacji obiektów warstwy usług lub każda zmiana wartości modelu danych powinna być logowana dla celów informacyjnych. Implementacja powyższych reguł może wyglądać tak, że dodawane są instrukcje logujące na początku i końcu działania metody. Ręczne wytwarzanie takiego kodu jest narażone na błędy oraz trudności związane ze zmianą reguł. W takim przypadku warto jest użyć jakiegoś automatu który wykona wymagane instrukcje, takim narzędziem jest PIAB.

Podobnie można postąpić w przypadku gdy obsługa wyjątków ma na celu realizację reguł typu: osłoń wyjątek bazy danych po przez zastąpienie go innym na poziomie warstwy usług lub ukryj każdy wyjątek na poziomie GUI, jednocześnie logując informacje o nim. Po zdefiniowaniu zasad obsługi reguł i wykorzystaniu istniejącej konfiguracji Application Block, PIAB automatycznie wykonywał będzie wskazane reguły.

W przypadku walidacji również można uprościć kod aplikacji gdy posiadamy spójny zestaw reguł walidowania. PIAB wspomoże również obsługę reguł postaci Każdy parametr metod warstwy usług musi być poprawny.

Warto dodać iż PIAB wykorzystuje istniejącą konfigurację VAB, EHAB i LAB dodając tylko same zasady ich aplikowania.

Sposób wykonania

Mając aplikację używającą poszczególnych Application Block, pierwszym krokiem jest zastanowienie się nad regułami jakie chcemy zaimplementować – sposobami jak automatyzować operacje w aplikacji. Kolejnym krokiem jest ustalenie sposobu określenia wytycznych w konfiguracji PIAB. Na końcu usuwamy istniejący kod poszczególnych Application Block i zmieniamy sposób tworzenia obiektów podlegających modyfikacjom.

Podsumowując mamy następujące kroki:

  1. Określenie reguł,
  2. Konfiguracja PIAB – konfiguracja wytycznych,
  3. Usunięcie istniejącego kodu VAB, EHAB i LAB,
  4. Zmiana sposobu tworzenia obiektów.

Przykład

Cały proces aplikowania PIAB zaprezentowany zostanie przy użyciu przykładowej aplikacji. Projekt ZineNet.PiabContest.Base to aplikacja 3 warstwowa używająca VAB, EHAB i LAB.

Kontrakt kodu usług bazuje na interfejsach – to znaczy funkcjonalność API usług jest definiowana za pomocą interfejsów. Tworzeniem instancji usług zajmuje się klasa ServiceFactory.

namespace ZineNet.PiabContest.Services {
  public interface IFooService {
    FooEntity GetById(Guid id);
    List<FooEntity> GetByNameStart(string namePart);
    FooEntity Set(FooEntity value);
    void Remove(Guid id);
  }
}
namespace ZineNet.PiabContest.Model {
  [HasSelfValidation]
  public sealed class FooEntity {
    public FooEntity() { _id = Guid.NewGuid(); }
    public FooEntity(Guid id, string name, DateTime from, DateTime to) {
      Id = id;
      Name = name;
      ValidFrom = from;
      ValidTo = to;
    }
    public Guid Id { get ...
      set {
        if (value == Guid.Empty)
        { throw new ArgumentException("FooEntity's Id attribute cannot be empty.", "value"); }
        _id = value;
        Logger.Write("Set FooEntity.Id", "General");
      }
    }
    [NotNullValidator]
    [RegexValidator(@"^ [A-Z] [a-zA-Z0-9-_\.]{3,15} $", RegexOptions.IgnorePatternWhitespace)]
    [StringLengthValidator(4, RangeBoundaryType.Inclusive, 16, RangeBoundaryType.Inclusive)]
nbsp;   public string Name { ... }
    [PropertyComparisonValidator("ValidTo", ComparisonOperator.LessThanEqual)]
    public DateTime ValidFrom { ... }
    [PropertyComparisonValidator("ValidFrom", ComparisonOperator.GreaterThanEqual)]
    public DateTime ValidTo { ... }
    [SelfValidation]
    public void ValidateThis(ValidationResults results) {
      if (_id == Guid.Empty) { ... }
      if (_from.Date != _from) { ... }
      if (_to.Date != _to) { ... }
    }
  }
}

Teoretycznie aplikacja składa się z warstwy dostępu do danych, na której działa warstwa usług. W celu uproszczenia przykładu, baza danych została zastąpiona statycznym słownikiem na obiekty FooEntity. Błędy dostępu do bazy danych symulują wyjątki pochodne klasy SystemException.

namespace ZineNet.PiabContest.Services {
  sealed class FooService: IFooService {
    private static Dictionary<Guid, FooEntity> _data = ...;
    public FooEntity GetById(Guid id) {
      FooEntity e = null;
      Logger.Write("Before FooService.GetById", "General");
      try {
        if (id == Guid.Empty) { throw new FakeSqlException("Id column of Foo table cannot be empty."); }
        e = _data[id];
      } catch (SystemException ex) {
        if (ExceptionPolicy.HandleException(ex, "WrapSystemExPolicy")) {
          throw;
        }
      }
      Logger.Write("After FooService.GetById", "General");
      return e;
    }
    public List<FooEntity> GetByNameStart(string namePart) {
      Logger.Write("Before FooService.GetByNameStart", "General");
      ...
      Logger.Write("After FooService.GetByNameStart", "General");
      return result;
    }
    public FooEntity Set(FooEntity value) {
      try {
        ValidationResults r = Validation.Validate(value);
        if (!r.IsValid) { throw new FakeSqlException("FooEntity validation failed."); }
      } catch (SystemException ex) {
        if (ExceptionPolicy.HandleException(ex, "WrapSystemExPolicy")) {
          throw;
        }
      }
      Logger.Write("Before FooService.Set", "General");
      ...
      Logger.Write("After FooService.Set", "General");
    }
    public void Remove(Guid id) {
      Logger.Write("Before FooService.Remove", "General");
      ...
      Logger.Write("After FooService.Remove", "General");
    }
    private sealed class FakeSqlException: SystemException {
      public FakeSqlException(string message)
        : base("SQL Database Error: " + message) {}
    }
  }
}

Konsumentem usług, a zarazem symulacją warstwy interfejsu użytkownika zajmuje się klasa EntryPoint.

namespace ZineNet.PiabContest {
  static class EntryPoint {
    static void Main() {
      QuickTests();
      try {
        ExCallService();
      } catch (Exception ex) {
        Console.WriteLine(" [EXCEPTION] " + ex.Message);
      }
      try {
        ExCallServiceSet();
      } catch (Exception ex) {
        Console.WriteLine(" [EXCEPTION] " + ex.Message);
      }
      try {
        ExModifyEntity();
      } catch (Exception ex) {
        Console.WriteLine(" [EXCEPTION] " + ex.Message);
      }
      Console.ReadKey();
    }
    private static void QuickTests() { ... }
    private static void ExCallService() { ... }
    private static void ExModifyEntity() { ... }
  }
}

Użycie poszczególnych Application Block polega na:

  1. VAB – walidacja stanu obiektów FooEntity – modelu danych.
  2. LAB – logowanie zmian obiektów modelu danych oraz wywołań metod usług.
  3. EHAB – osłona wyjątków z warstwy dostępu do danych oraz ukrycie szczegółów wyjątków przed użytkownikiem.

Diagram klas przykładowej aplikacji

Diagram klas przykładowej aplikacji

Definiowanie reguł i wytycznych

Reguły określone przez architekturę systemu to:

  1. Logowanie wywołań metod warstwy usług
  2. Logowanie zmian stanu modelu danych
  3. Osłona wyjątków warstwy dostępu do danych (symulowanych przez wyjątki pochodne SystemException)
  4. Osłona wyjątków warstwy usług
  5. Osłona wyjątków modelu danych.
  6. Walidacja stanu modelu danych przekazywanych do warstwy usług

Ponieważ interfejsy usług znajdują sie w jednej przestrzeni nazw wraz z innymi typami danych, możemy zdefiniować następującą wytyczną:

A. Każda metoda z każdego typu CLR danych o nazwie IFooService jest interesująca

Zmiana stanu modelu danych może być monitorowana po przez przechwycenie wywołania metody set na odpowiedniej właściwości. Oznaczamy każdą metodę set właściwości nas interesującej tagiem o nazwie ZineNet.PiabContest. Definiujemy następującą wytyczną:

B. Każda metoda oznaczona tagiem o nazwie ZineNet.PiabContest jest interesująca.

Reguły 3, 4 i 6 definiują również poprzednią wytyczną - A.

Reguła 5 definiuje poprzednią wytyczną – tym razem B.

Konfiguracja PIAB

Edytujemy plik konfiguracyjny przy użyciu znanego narzędzia Enterprise Library Configuration Editor.

  1. Dodajemy Policy Injection Application Block
  2. W nim tworzymy dwie wytyczne (Policy) o nazwach ByTagPolicy i ServicePolicy
    Widok ogólny na konfigurację
  3. Do wytycznej ByTagPolicy dodajemy regułę dopasowania według TagAttribute (Tag Attribute Matching Rule) i ustawiamy wartości następująco:
    Ustawienia dla wytycznej ByTagPolicy 1
  4. Do wytycznej ServicePolicy dodajemy regułę dopasowania według typu CLR (Type Matching Rule) i ustawiamy właściwości następująco:
    Ustawienia dla wytycznej ServicePolicy 1
    Matches:
    Ustawienia dla wytycznej ServicePolicy 2

Kolejnym krokiem jest dodanie rodzaju obsługi (Handlers) do wytycznych

  1. Do wytycznej ByTagPolicy dodajemy obsługę logowania (*.Log) i wyjątków (*.Exception) – potrzebne dla reguł 2 i 5. Ustawiamy je w następujący sposób:
    Ustawienia dla wytycznej ByTagPolicy 2

    Ustawienia dla wytycznej ByTagPolicy 3
  2. Do wytycznej ServicePolicy dodajemy obsługę logowania (*.Log), wyjątków (*.Exception) i walidacji (*.Validation) – potrzebne dla reguł 1, 3, 4 i 6. Ustawiamy je w następujący sposób:
    Ustawienia dla wytycznej ServicePolicy 3

    Ustawienia dla wytycznej ServicePolicy 4

    Ustawienia dla wytycznej ServicePolicy 5

Usunięcie istniejącego kodu Application Block

Usuwamy kod walidacji, logowania i przechwytywania wyjątków z klas FooService, FooEntity oraz EntryPoint.

public FooEntity GetById(Guid id) {
  FooEntity e = null;
  Logger.Write("Before FooService.GetById", "General");
  try {
    if (id == Guid.Empty) { throw new FakeSqlException("Id column of Foo table cannot be
empty."
); }
      e = _data[id];
  } catch (SystemException ex) {
    if (ExceptionPolicy.HandleException(ex, "WrapSystemExPolicy")) {
      throw;
    }
  }
  Logger.Write("After FooService.GetById", "General");
  return e;
}
...
public string Name {
    get { return _name; }
    set {
    _name = value;
    Logger.Write("Set FooEntity.Name", "General");
  }
}

Po modyfikacji

public FooEntity GetById(Guid id) {
  if (id == Guid.Empty) { throw new FakeSqlException("Id column of Foo table cannot be empty."); }
  return _data[id];
}
...
public string Name { get ... [Tag("ZineNet.PiabContest")] set ... }

Dopasowania

Bazując na prostej aplikacji przedstawię sposób użycia kilku podstawowych dopasowań.

Aplikacja ma zdefiniowane 2 typy danych, które będą podlegały zmianom przez PIAB.

Typy danych podlegające modyfikacjom PIAB

namespace ZineNet.PiabContest.MyMatches {
  namespace SubA {
    public sealed class Aaa : MarshalByRefObject {
      ...
      public string Name {
        [Tag("myGet")] get { return _name; }
        [Tag("mySet")] set { _name = value; }
      }
    }
    public sealed class Aaa_111 : MarshalByRefObject {
      ...
      public string Name {
        [Tag("myGet")] get { return _name; }
        [Tag("mySet")] set { _name = value; }
      }
    }
  }
  namespace SubB {
    public sealed class Bbb : MarshalByRefObject {
      ...
      public string Name {
        [Tag("myGet")] get { return _name; }
        [Tag("mySet")] set { _name = value; }
      }
    }
    public sealed class Bbb_111 : MarshalByRefObject {
      ...
      public string Name {
        [Tag("myGet")] get { return _name; }
        [Tag("mySet")] set { _name = value; }
      }
    }
  }
}

Testowa metoda tworzy instancję każdego z typów za pomocą klasy PolicyInjection. Następnie na każdym obiekcie wykonywana jest operacja get/set:

o.Name = o.Name;

Podczas tworzenia obiektu przekazywana jest dodatkowo konfiguracja PIAB wczytana z pliku App_*.config znajdującego w katalogu aplikacji. Dzięki temu można stworzyć, bądź edytować plik konfiguracyjny i obserwować zmiany bez potrzeby rekompilacji aplikacji.

Podstawowy plik konfiguracyjny ma zdefiniowane logowanie na konsole aplikacji.

By Assembly

Konfiguracja

<matchingRules>
  <add name="Assembly Matching Rule"type="...AssemblyMatchingRule..."
       match="ZineNet.PiabContest.MyMatches" />
</matchingRules>

Wynik

  SubA.Class1.get_Name #
  SubA.Class1.set_Name # value - A_Class1;
  SubA.Class2.get_Name #
  SubA.Class2.set_Name # value - A_Class2;
  SubB.Class1.get_Name #
  SubB.Class1.set_Name # value - B_Class1;
  SubB.Class2.get_Name #
  SubB.Class2.set_Name # value - B_Class2;

Parametrem reguły jest ZineNet.PiabContest.MyMatches czyli każdy typ tworzony/owijany przez PIAB będzie logowany.

By MemberName

Konfiguracja

<matchingRules>
  <add name="Member Name Matching Rule" type="...MemberNameMatchingRule..." >
    <matches>
      <add match="get_*" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class2.get_Name #
  SubB.Class1.get_Name #
  SubB.Class2.get_Name #

Parametrem reguły jest get_* czyli każda metoda zaczynająca się od get_ przez PIAB będzie logowana. Właściwości C# tłumaczone są przez kompilator na metody odpowiednio get_ lub set_.

By Namespace

Konfiguracja

<matchingRules>
  <add name="Namespace Matching Rule" type="...NamespaceMatchingRule...">
    <matches>
      <add match="ZineNet.PiabContest.MyMatches.SubA" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class1.set_Name # value - A_Class1;
  SubA.Class2.get_Name #
  SubA.Class2.set_Name # value - A_Class2;

Parametrem reguły jest ZineNet.PiabContest.MyMatches.SubA czyli każdy typ tworzony/owijany przez PIAB z tej przestrzeni nazw będzie logowany.

By Property

Konfiguracja

<matchingRules>
  <add name="Property Matching Rule" type="...PropertyMatchingRule..." >
    <matches>
      <add match="Name" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class1.set_Name # value - A_Class1;
  SubA.Class2.get_Name #
  SubA.Class2.set_Name # value - A_Class2;
  SubB.Class1.get_Name #
  SubB.Class1.set_Name # value - B_Class1;
  SubB.Class2.get_Name #
  SubB.Class2.set_Name # value - B_Class2;

Parametrem reguły jest Name czyli każda właściwość o tej nazwie będzie przez PIAB będzie logowana.

By TagAttribute

Konfiguracja

<matchingRules>
  <add name="Tag Attribute Matching Rule"
       match="myGet" ignoreCase="false" type="...TagAttributeMatchingRule..."/>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class2.get_Name #
  SubB.Class1.get_Name #
  SubB.Class2.get_Name #

Parametrem reguły jest myGet czyli każdy element posiadający atrybut [Tag] o wartości myGet będzie przez PIAB logowany. W tym przypadku będą to metody get właściwości.

By TypeName

Konfiguracja

<matchingRules>
  <add name="Type Matching Rule" type="...TypeMatchingRule..." >
    <matches>
      <add match="Class1" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class1.set_Name # value - A_Class1;
  SubB.Class1.get_Name #
  SubB.Class1.set_Name # value - B_Class1;

Parametrem reguły jest Class1 czyli każdy element typu o tej nazwie będzie przez PIAB logowany. W tym przypadku będą to metody get właściwości.

Complex

Dopasowania można łączyć za pomocą operatora AND, po prostu dodając wiele elementów do węzła Matches. Bardziej złożone warunki można tworzyć przy użyciu projektu EntLibContrib, lub za pomocą własnych dopasowań.

Konfiguracja

<matchingRules>
  <add name="Member Name Matching Rule" type="...MemberNameMatchingRule...">
    <matches>
      <add match="get_*" ignoreCase="false" />
    </matches>
  </add>
  <add name="Namespace Matching Rule" type="...NamespaceMatchingRule...">
    <matches>
      <add match="ZineNet.PiabContest.MyMatches.SubB" ignoreCase="false" />
    </matches>
  </add>
  <add name="Type Matching Rule" type="...TypeMatchingRule...">
    <matches>
      <add match="Class2" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubB.Class2.get_Name #

W tym przypadku logowaniu poddane zostaną typy z przestrzeni nazw ...SubB o nazwie Class2 i to tylko metody o nazwie zaczynającej sie od get_.

Własny Handler i Matching Rule

Własny Handler będzie wypisywał tylko linijkę tekstu na konsolę, przed i po wywołaniu metody.

namespace ZineNet.PiabContest.MyMatches {
  [ConfigurationElementType(typeof(CustomMatchingRuleData))]
  public sealed class MyCutomMatching : IMatchingRule {
    public MyCutomMatching(NameValueCollection notUsed) {}
    public bool Matches(MethodBase member) {
      bool nsMatches = member.DeclaringType.Namespace == typeof(SubA.Class2).Namespace;
      bool typeMatches = member.DeclaringType.Name == typeof(SubA.Class2).Name;
      bool nameMatches = member.Name == "set_Name";
      return nsMatches && (typeMatches || nameMatches);
    }
  }
  [ConfigurationElementType(typeof (CustomCallHandlerData))]
  public class MyCustomHandler: ICallHandler {
    public MyCustomHandler(NameValueCollection notUsed) { }
    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext) {
      Console.WriteLine("{");
      IMethodReturn msg = getNext()(input, getNext);
      Console.WriteLine("}");
      return msg;
    }
  }
}

Konfiguracja

<matchingRules>
  <add name="Custom Matching Rule"
       type="ZineNet.PiabContest.MyMatches.MyCutomMatching, ZineNet.PiabContest.MyMatches"
  />
</matchingRules>
<handlers>
  <add name="Custom Handler"
       type="ZineNet.PiabContest.MyMatches.MyCustomHandler, ZineNet.PiabContest.MyMatches"
  />
  ...
</handlers>

Wynik

{
  SubA.Class1.set_Name # value - A_Class1;
}
{
  SubA.Class2.get_Name #
}
{
  SubA.Class2.set_Name # value - A_Class2;
}

Reguła dopasowania określa każdy typ z przestrzeni nazw SubA, którego nazwa to Class2 lub metoda/właściwość set_Name.

Zadanie konkursowe #4 – Policy Injection Application Block, Enterprise Library 3.1

Zgodnie z założeniami konkursu na koniec zadania do wykonania.

Zgodnie z opisanymi powyżej krokami zrefaktoryzować kod aplikacji konkursowej tak, aby wyeliminować jak najwięcej powtarzalnego kodu logowania, obsługi błędów i walidacji. W szczególności:

  1. Sposób logowania zgodny z zadaniem konkursowym #1 oraz:
    • Warstwa usług powinna logować wszystkie swoje operacje – zarówno te z IDataService{T} jak i interfejsów pochodnych.
    •  Powinna logować sukces (po wykonaniu kodu).
  2. Sposób obsługi błędów zgodny z zadaniem konkursowym #2
    • Każda z operacji usług powinna chronić wyjątki generowane przez warstwy niższe jak i samą siebie.
    •  Należy użyć wyjątku specjalnie dedykowanego operacjom na usługach..
    • Warstwa prezentacji powinna mieć wyciszanie wyjątków dla każdego handlera *Click lub *DoubleClick.
    •  Zawartość błędu powinna być logowana (z warstwy prezentacji oraz usług) oraz ogólna wiadomość wyświetlona użytkownikowi.
  3. Sposób walidacji zgodny z zadaniem konkursowym #3
    • Jeżeli obiekt posiadający zdefiniowane reguły walidacji jest przekazywany do usługi, musi być poprawny.

Ponieważ realizacja tych założeń jest zależna od poprzednich konkursów, więc można wysłać rozwiązanie na każdy z trzech punktów (LAB, EHAB, WAB). Rozwiązanie każdego z powyższych punktów to osobna szansa na wygraną, warto więc pokusić się o wszystkie 3 punkty i 3 razy trafić do 'szczęśliwego kapelusza'.

Rozwiązane zadania należy przesyłać na adres mgrzeg at gmail kropka com. Reguły wiadomości:

Temat wiadomości: [ENTLIB PIAB] Rozwiązanie zadania konkursowego;

Treść (może być w załączonym pliku .txt, .doc, .rtf) powinna zawierać krótki opis co zostało wykonane;

Plik .zip zawierający kod aplikacji konkursowej po refaktoryzacji. Można dołączyć również kod wykonywalny (bez bibliotek EntLib, etc. - tylko to, co niezbędne);

Dane adresowe do wysyłki nagrody - NIE! Skontaktujemy się ze "szczęśliwcem" via email i ustalimy szczegóły.

Adresy:

Kod aplikacji przykładowej opisywanej w tekście

Kod aplikacji konkursowej

UPDATE: Ostateczny termin przysyłania rozwiązań to 5 listopada 2007 roku.

W razie wątpliwości etc. proszę zadawać pytania.

Powodzenia!

opublikowano 15 października 07 04:40 przez Wojciech Gebczyk | 0 komentarzy   
Zarejestrowano w kategorii: , , , , ,
[Ww.Xaml] Xaml i Wpf
Przy okazji pracy nad paroma rzeczami w WPFie, da sie zauwazyc rozdzielnosc XAMLa i WPFa rozumianego jako frameworka UI. XAMLa mozna wykorzystac nie tylko w WPFie ale i na przyklad rowniez w WinFormsach

Kod:
using System.Windows.Markup;
using System.Windows.Forms;
using System.IO;
using System.Xml;

namespace Ww.WpfAndWinForms {
    static class EntryPoint {
        static void Main() {
            string formsXaml = @"
                <Form xmlns='clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms'
                    FormBorderStyle='FixedSingle'
                    MaximizeBox='false'
                    MinimizeBox='false'
                    ClientSize='240,150'
                    StartPosition='CenterScreen'
                >
                    <Form.Controls>
                        <Label Text='Choose color:' Top='8' Left='16' />
                        <RadioButton Text='Red' Top='28' Left='24' />
                        <RadioButton Text='Green' Top='48' Left='24' />
                        <RadioButton Text='Blue' Top='68' Left='24' />
                        <RadioButton Text='None' Top='88' Left='24' Checked='true' />
                        <Button Text='Accept' Top='118' Left='16' Width='100' />
                        <Button Text='Cancel' Top='118' Left='124' Width='100' />
                    </Form.Controls>
                </Form>";
            using (XmlReader r = XmlReader.Create(new StringReader(formsXaml))) {
                object o = XamlReader.Load(r);
                System.Windows.Forms.Application.Run((Form)o);
            }
        }
    }
}



Plik csproj (fragment):
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Windows.Forms" />
    <Reference Include="System.XML" />
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>

Dopoki wymagania XAMLa w stosunku do obiektow sa spelnione to taka deserializacja bedzie dzialala.

Inny pomysl jaki mi przychodzi do glowy to uzycie XAMLa jako formatu wiadomosci jakiegos rodzaju uslug. Gdzie poza standardowymi "tagami" obiektow mozna by wytworzyc extensions i jakos ciekawie wykorzystac (lepsza walidacja? jakies metadane?). Nie mam nic konkretnego na mysli, ale moglo by cos z tego wyniknac.
opublikowano 26 lipca 07 01:01 przez Wojciech Gebczyk | 5 komentarzy   
Zarejestrowano w kategorii: , , , ,
[Ww.Note] Specyfikacja po przez implementacje?
Przegladajac poranna porcje newsow blogowych natrafilem na artykul MFowlera o Ruby+Microsoft. Rozbawilo mnie jedno zdanie szczegolnie:
"Soon-to-be-ThoughtWorker Ola Bini, a JRuby committer, reckons that it's almost impossible to figure out how to implement a Ruby runtime without looking at source code of the MRI (...)".
No jesli ten Ruby ma tak dobra specyfikacje to ja dziekuje za taki jezyk. Wyglada jakby jezyk zostal "narzezbiony" na kolanie (i tak wyglada z przykladow) po przez wrzucanie kolejnych cukierkow do ogrodka. I jak to sie ma do ostatnich walk (troche na innym polu: uslug sieciowych) aby kontrakt nie byl definiowany przez kod ;-)

Ten rubin ma chyba jakas skaze... pewnie zarazil sie nia od web "programistow" JavaScriptowych.

Tak przy okazji to w sumie niezle podejsce dla Javowych zealotow: Zrobmy jezyk, nawolujmy do jego implementacji przez MS bo jest "cool" ale poniewaz specyfikacja jest slaba to bedzeimy zawsze moglo powiedziec ze MS stara sie forsowac swoja implementacje ;-)
opublikowano 13 czerwca 07 09:43 przez Wojciech Gebczyk | 6 komentarzy   
Zarejestrowano w kategorii: ,
Ww.Texts.TextTemplates

Kontynuując temat kompilatorów, chciałbym opisać technikę tworzenia szablonów tekstowych. Szablony tekstowe mogą zostać użyte w przypadku, gdy chcemy umożliwić użytkownikowi aplikacji szeroką możliwość definiowania zawartości niektórych dokumentów. Szablony pozwalają również na zmiany w aplikacji bez potrzeby jej przekompilowywania. Pod pojęciem szablonu tekstowego rozumiem wzorzec, szkic, z którego powstanie finalny dokument tekstowy. Z podobnych, istniejących rozwiązań mogę wymienić na przykład, Apache Velocity (NVelocity), CodeSmith, Smarty lub nowe rozwiązanie Microsoftu T4 z DSL Tools. Za przykład mogą również posłużyć wszelkie rozwiązania ery post-CGI, takie jak PHP, ASP czy JSP. Również w ASP.NET podstawą są szablony tekstowe, lecz tak bardzo rozbudowane, że sama funkcjonalność generowania tekstu z szablonu niknie w morzu funkcjonalności w rodzaju “code behind”, “user controls” czy “post back”. Lecz nadal na samym dnie, można doszukać się idei szablonów tekstowych. Celem artykułu jest zaprezentowanie idei dynamicznego generowania fragmentów tekstu przy użyciu pewnego języka szablonów.

Definicja języka szablonów

Dla potrzeb artykułu stworzony zostanie prosty język definiowania szablonów tekstowych. Pozwalał będzie na wplatanie kodu w języku C# i definiowanie nazwanych parametrów. Zacznijmy od przykładu prostego szablonu:

Code 1. - Przykładowy szablon tekstowy generujący listę wiadomości

<%@ @uses="System, System.Collections.Generic" @debug="true"
Messages="List<string>"
FirstName="string"
LastName="string"
%>
Welcome <%= LastName %>, <%= FirstName %>.
You have <%= Messages.Count %> new message(s):
<% foreach (string msg in Messages) { %>
- "<%= msg %>"<%
}
%>

Have a nice day.

Zadaniem szablonu jest stworzenie notki powitalnej z listą nowych wiadomości. Jeżeli przekazalibyśmy do szablonu następujące parametry:

Code 2. - Fragment kodu prezentującego ideę użycia szablonu w kodzie
ITemplate tpl = CreateTemplate(...);

tpl["Messages"] = new List<string>(new string[] {
    "Re: templates",
    "New better Java 1.82!",
    "Latest topic on dev.pl",
});
tpl["FirstName"] = "Jan";
tpl["LastName"] = "Kapusta";

To należy oczekiwać następującego wyniku:
Code 3. - Wynik przykładowego szablonu z listingu 1

Good Morning Kapusta, Jan.
You have 3 new message(s):

- "Re: templates"
- "New better Java 1.82!"
- "Latest topic on dev.pl"

Have a nice day.

Taki szablon możemy zapisać na dysku w katalogu aplikacji i wczytać przy starcie. Umożliwia to podmianę szablonu, bez potrzeby rekompilowania całej aplikacji oraz umożliwia dostosowanie do potrzeb konkretnego użytkownika.
Teraz zostaną dokładniej opisane oczekiwania względem tego rozwiązania, czyli składnię języka szablonów tekstowych. Pierwszym elementem jest opcjonalna definicja szablonu:

Code 4. - Definicja nagłówka szablonu

[
“<%@”
[“@class” “=” ‘"’ <template base class name> ‘"’ ]
[“@uses” “=” ‘"’ <namespaces coma separated> ‘"’ ]
[“@imports” “=” ‘"’ <assembly file names coma separated> ‘"’ ]
[<parameter name> “=” ‘"’ <parameter value> ‘"’ ] *
“%>”
]

Element zaczyna się od “<%@”, potem musi istnieć przynajmniej jeden biały znak, a kończy się tagiem “%>” poprzedzonym, co najmniej jednym białym znakiem. Zawartość tej definicji szablonu składa się z par nazwa-wartość, gdzie wartość jest ujęta w cudzysłowy, podobnie jak atrybuty w XMLu. Wyjątkiem są 3 specjalne atrybuty służące do konfigurowania kompilacji szablonu. Tag definicji szablonu jest opcjonalny i szablon może wyglądać tak:

Code 5. - Szablon tekstowy bez zdefiniowanego nagłówka

Liczymy od 0 do 10!
0<% for (int i = 1; i <= 10; i++) { Output.Write(“, {0}”, i); } %>.
Koniec.

W treści szablonu możemy dowolnie zagnieżdżać zwykły tekst i tagi specjalne takie jak kod języka C# bądź literał do wypisania na wyjściu.
Do zagnieżdżania kodu języka C# służą dwa elementy: “<%” oraz “%>”.

Code 6. - Definicja tagów służących do zagnieżdżania kodu w języku C#

“<%” <escaped C# code> “%>”

Pierwszy tag rozpoczyna blok kodu języka C# i po nim musi wystąpić, co najmniej jeden biały znak oddzielający od tekstu kodu. Blok kodu kończy się podobnym elementem “%>” tym razem poprzedzonym, co najmniej jednym białym znakiem. Wewnątrz może znajdować się dowolny kod języka C#. Jeżeli chcemy użyć znaku procent “%”, musimy zapisać go jako podwójny znak procent “%%”. Wewnątrz bloku kodu możemy używać każdego nie prywatnego elementu tej klasy, między innymi właściwości “Output”, do której możemy zapisać dowolny tekst.
Aby uniknąć używania za każdym razem właściwości Output i metody Write lub WriteLine, zdefiniowany został na podobieństwo ASP, następujący skrót rozpoczynający się od “<%=” i kończący elementem “%>”.

Code 7. - Definicja tagów służących do wypisywania wartości wyrażeń

 “<%=” <escaped C# code expression> “%>”

Podobnie jak poprzednio pierwszy element musi być zakończony białym znakiem, a ostatni poprzedzony białym znakiem. Obie poniższe linie są równoważne:

Code 8. - Konstrukcje wypisywania wartości wyrażeń

<% Output.Write(i); %>
<%= i %>

Wszystkie 3 przypadki użycia tagów szablonu (“<%”, “<%@”, “<%=”) można uogólnić do przypadku gdzie tag otwierający zaczyna się od “<%” i po nim następuje opcjonalny znak decydujący, jakie rozszerzenie jest użyte. Po tym opcjonalnym trzecim znaku ponownie musi wystąpić biały znak oddzielający zawartość tagu od samego tagu. Każdy z takich tagów kończy się białym znakiem oraz elementem “%>”. Takie rozwiązanie umożliwia dalsze rozszerzanie o nowe ułatwienia. W ASP.NET w podobny sposób definiuje się wiązanie danych (bindowanie) przy użyciu znaku “#” jako “rozszerzenia”.

Projekt implementacji

Kolejnym krokiem jest zastanowienie się nad implementacją powyższego rozwiązania. Fragment przykładowego użycia można zobaczyć w listingu 2. Musi istnieć możliwość łatwego przekazywania parametrów do szablonu. Należy umożliwić również kompilację szablonów z różnych źródeł: plików tekstowych, zasobów bibliotek, zmiennych tekstowych, itp. Należy pomyśleć o możliwości definiowania dla szablonów nowych rozszerzeń.
W zaproponowanym rozwiązaniu jedna statyczna klasa zajmie się zarządzaniem szablonami, ukrywając cały proces kompilacji przed programistą. Klasa parsera tekstu szablonu zajmie się parsowaniem i tworzeniem obiektu struktury szablonu. Klasa kompilatora szablonów umożliwi kompilację struktury szablonu do typu .NET implementującego interfejs szablonu tekstowego. Wydzielone zostaną odpowiednie interfejsy dla szablonów i rozszerzeń szablonów.
Cały proces od definicji szablonu tekstowego do uzyskania wyniku zaczynał będzie się od sparsowania treści szablonu i wyprodukowania obiektowej jego struktury. Kolejnym etapem jest uzyskanie typu implementującego interfejs szablonu a reprezentującego sparsowany szablon. W tym celu kompilator przekształci model szablonu w kod klasy języka C#, a następnie skompiluje kod i zwróci wygenerowany typ. W kolejnym kroku uzyskany typ .NET zostanie zinstancjonowany, uzupełniony parametrami i wykonany - wyrenderowany.

Na rysunkach 9, 10 i 11 przedstawione zostały schematy klas należących do projektu.

Picture 9. - Główne klasy generatora szablonów

Picture 10. - Struktura klas rozrzerzeń szablonów

Picture 11. - Struktura szablonu

Interfejs szablonu

Zacznijmy od zdefiniowania interfejsu dla szablonów:

Code 12. - Interfejs szablonu tekstowego

public interface ITemplate {
    void Render(TextWriter output);
    string RenderToString();
    object this[string name] { get; set; }
}

Zdefiniowany indekser pozwala na manipulowanie parametrami szablonu. Dwie metody komponentu renderującego pozwalają na optymalizację różnych scenariuszów użycia. Metoda RenderToString zwraca nam gotowy ciąg tekstowy, gdzie nie musimy zajmować się strumieniami tekstowymi. Druga z metod Render przyjmuje parametr typu TextWriter, do którego treść szablonu zostanie zapisana. W przypadku zapisu do pliku czy do konsoli, możemy w taki sposób uniknąć nie potrzebnej alokacji pamięci.
Klasa implementująca powyższy interfejs powinna pamiętać o udostępnieniu właściwości o nazwie Output i typie TextWriter. Powinna wskazywać na obiekt, który jest przekazywany do metody Render.
Model szablonu
Reprezentacją obiektową szablonu będzie klasa TemplateModel. Ta klasa będzie właściwą strukturą, na której w kolejnym kroku działał będzie kompilator. Model szablonu powinien zawierać wszystkie elementy niezbędne do wygenerowania finalnego kodu klasy języka C#. Na tym etapie wszelka interpretacja zapisów szablonu powinna zostać zakończona i gotowa do użycia przez kompilator szablonów.

Code 13. - Klasa modelu szablonu

public sealed class TemplateModel {
    public string BaseClass { get; set; }
    public bool Debug { get; set; }
    public List<string> Imports { get; set; }
    public List<string> Uses { get; set; }

    public Dictionary<string, string> Parameters { get; set; }
    public List<ITemplateRenderer> Renderers { get; set; }
}

Właściwości klasy modelu szablonu można podzielić na dwie grupy. Pierwsza to konfiguracja kompilacji kodu języka C#, a druga odpowiada za konfigurację treści szablonu.
Szablon ma zdefiniowane następujące elementy konfigurujące kompilację:

  • Nazwa bazowej klasy (właściwość BaseClass i atrybut @baseClass szablonu tekstowego). Jeżeli wartość nie jest zdefiniowana, użyta zostanie wtedy standardowa klasa TemplateBase.
  • Zmienną określającą czy szablon powinien zostać kompilowany w trybie Debug (właściwość Debug i atrybut @debug szablonu tekstowego). Kompilacja w trybie debug pozostawi tymczasowe pliki w katalogu tymczasowym.
  • Lista zaimportowanych dodatkowych bibliotek (właściwość Imports i atrybut @imports szablonu tekstowego). Jest to lista nazw plików bibliotek separowana średnikami, która zostanie dołączona podczas kompilacji klasy języka C#.
  • Lista zaimportowanych przestrzeni nazw (właściwość Uses i atrybut @uses szablonu tekstowego). Jest to lista plików bibliotek separowana średnikami, która zostanie dołączona podczas kompilacji klasy języka C#.
  • Zdefiniowane zostały dwie właściwości odpowiadające za treść szablonu:
  • Słownik z parametrami szablonu (właściwość Parameters i wszystkie pozostałe atrybuty szablonu tekstowego). Jest to kolekcja nazwa-wartość, gdzie kluczem jest nazwa parametru, a wartością jego typ.
  • Lista komponentów renderujących treści szablonu (właściwość Renderers) reprezentująca właściwą treść szablonu.
  • Każde z rozszerzeń szablonów powinno mieć zdefiniowany unikalny znak rozszerzenia oraz metodę wytwarzającą właściwy komponent renderujący, który musi mieć metodę generującą kod klasy proxy szablonu.

Code 14. - Interfejsy rozrzerzeń szablonów

public interface ITemplateExtension {
    char ExtensionChar { get; }
    ITemplateRenderer CreateRenderer(string content);
}

public interface ITemplateRenderer {
    void Render(TextWriter writer);
}

Parsowanie szablonu

Przekształcaniem tekstu szablonu do jego modelu zajmuje się klasa TemplateParser. Nie jest to pełnoprawny lekser i parser, lecz jedna, uproszczona klasa. Parsowanie odbywa się rekurencyjnie od obiektu szablonu do poszczególnych parametrów i komponentów renderujących. Przetwarzanie poszczególnych elementów odbywa się za pomocą wyrażeń regularnych, które rozbijają poszczególne „tagi” (<% i %>) na elementy składowe.

Code 15. - Klasa parsera treści szablonu

public sealed class TemplateParser {
    public static TemplateModel Parse(TextReader reader);
}

Klasa parsera posiada jedną publiczną, statyczną metodę Parse, która przyjmuje na wejściu treść szablonu i generuje obiekt modelu. Rysunek 16 prezentuje sekwencję działania parsera.

Picture 16. - Diagram sekwencji opisujący działanie parsera

Kompilacja

Kompilacja jest bardzo podobna do tej z poprzedniego artykułu. Warto wspomnieć o tym, że przetwarzanie komponentów renderujących odbywa się po przez metodę Render interfejsu ITemplateRenderer.

Code 17. - Klasa kompilatora szablonów

public sealed class TemplateCompiler {
    public static Type Compile(TemplateModel model);
    public static Dictionary<TemplateModel, Type> Compile(params TemplateModel[] models);
}

Upublicznione zostały dwie metody zajmujące się kompilacją. Pierwsza przyjmuje na wejściu model szablonu i zwraca skompilowany typ. Druga przyjmuje na wejściu tablicę modeli i zwraca kolekcję skompilowanych typów. W drugim przypadku kompilacja wszystkich modeli przebiega jednoetapowo – generowane jest jedno assembly, więc oszczędzane są zasoby systemowe.

Klasa TemplateManager

Poszczególne klocki mamy już zdefiniowane i teraz parę zdań o klasie zarządzającej szablonami. Klasa ta ukrywa każdy z etapów przetwarzania szablonu, udostępniając najczęściej używaną funkcjonalność w postaci łatwo dostępnych, statycznych metod.

Code 18. - Klasa zarządzająca operacjami na szablonach

public static class TemplateManager {
    public static void RegisterExtension(

        ITemplateExtension extension);
    public static void UnregisterExtension(

        ITemplateExtension extension);
    public static ITemplateExtension GetExtension(char extChar);

    public static Type GetTemplateType(TextReader reader);
    public static ITemplate GetTemplate(TextReader reader);
    public static string RenderTemplate(TextReader reader);
    public static string RenderTemplate(TextReader reader,

        IDictionary<string, object> parameters);
    public static string RenderTemplate(TextReader reader,

       
params KeyValuePair<string, object>[] parameterList);
}

Klasa zarządzająca szablonami posiada dwie główne funkcjonalności: zarządzanie rozszerzeniami oraz kompilację szablonów.
Manipulacja rozszerzeniami szablonów tekstowych – dodawanie, usuwanie oraz pobieranie rozszerzeń – odbywa się za pomocą metod RegisterExtension, UnregisterExtension i GetExtension. Metoda GetTemplateType kompiluje szablon tekstowy i zwraca w wyniku swojego działania typ klasy proxy dla szablonu tekstowego. Metoda GetTemplate generuje instancję klasy szablonu tekstowego, gotową do użycia.
Zbiór przeładowanych metod RenderTemplate ma za zadanie kompilację i wykonanie szablonu tekstowego. Zwracaną wartością jest wynik wygenerowanego szablonu tekstowego.

Generator klas - TextTemplates.ClassGen

Za praktyczny przykład zastosowania tego rozwiązania posłuży proste narzędzie do generowania treści klas na podstawie definicji z pliku XML. Podobnych narzędzi dostępnych jest wiele (na przykład MyGeneration, Sooda) i posiadają znacznie większe możliwości. Zadaniem projektu jest zaprezentowanie jednego ze sposobów użycia szablonów tekstowych.
Narzędzie przyjmowało będzie na wejściu plik XML z definicją encji i generowało będzie plik z klasa w języku C#. Definicja wyglądu generowanej klasy jest umieszczona w szablonie tekstowym – Class.tpl.txt. Podczas działania programu plik XML jest przekształcany do obiektu Entity za pomocą standardowej serializacji XML, który to obiekt jest potem przekazywany do szablonu w celu dalszego przetwarzania.
Przykładowa definicja encji:

Code 19. - Przykładowy plik konfiguracyjny dla generatora klas

<?xml version="1.0" encoding="utf-8" ?>
<entity
 name="ExampleEntity"
 namespace="Ww.Texts.TextTemplates.ExampleNS"
 xmlns="http://ww/texts/textTemplates/classGen/"
>
 <a name="FirstName">string</a>
 <a name="LastName">string</a>
 <a name="Age">string</a>
 <a name="Contacts">List&lt;string&gt;</a>
</entity>

A to wynik działania programu:

Code 20. - Przykładowy wynik działania generatora klas.

using System.Collections.Generic;

namespace Ww.Texts.TextTemplates.ExampleNS {
  public class ExampleEntity {

    private string _firstName;
    private string _lastName;
    private string _age;
    private List<string> _contacts;

    public ExampleEntity() { }
    public ExampleEntity(string firstName, string lastName,

       
string age, List<string> contacts)
    {
      _firstName = firstName;
      _lastName = lastName;
      _age = age;
      _contacts = contacts;
    }

    public string FirstName
    { get { return _firstName; } set { _firstName = value; } }
    public string LastName
    { get { return _lastName; } set { _lastName = value; } }
    public string Age
    { get { return _age; } set { _age = value; } }
    public List<string> Contacts
    { get { return _contacts; } set { _contacts = value; } }
  }
}

Podsumowanie

W tym artykule pokazano w jaki sposób działają procesory szablonów tekstowych. Separacja samego procesu generowania tekstu od sterowania procesem generacji, pozwala zwiększyć przejrzystość kodu. Skorzystanie z kompilatora języka C# pozwala na etapie kompilacji projektu (kod w klasie bazowej szablonów) lub działania programu wyeliminować sporą część błędów programistycznych. Wydzielenie funkcjonalności rozszerzeń szablonów umożliwia dalszą optymalizację pracy programisty, po przez stosowanie wygodniejszych rozszerzeń podczas pracy z szablonami.

opublikowano 28 stycznia 07 04:23 przez Wojciech Gebczyk | 0 komentarzy   
Zarejestrowano w kategorii: , , , , , , ,