niedziela, 19 września 2010

Podstawy MVVM Light Toolkit [część 1]

Instalacja MVVM Light Toolkit
Właściwie jedyne co jest niezbędne do pracy z MVVM LT, to same biblioteki w formie plików DLL, jednak aby naprawdę wygodnie korzystać z tego narzędzia dobrze jest skorzystać z małych wspomagaczy przygotowanych przez autora. Pobieramy paczkę stąd i następnie wypakowujemy to co nam potrzeba. Jest tego całkiem sporo, ponieważ przygotowane są wersje zależne od wykorzystywanej technologii i oprogramowania:

  • binarki - biblioteki w postaci plików DLL w postaciach przygotowanych dla WPF'a w wersji 3.5 i 4.0, Silverlight'a 3.0 oraz 4.0 a także dla Windows Phone 7
  • szablony projektów - szablony w wersjach przeznaczonych zarówno dla Expression Blend (również wersja 3 i 4) oraz dla Visual Studio (osobno dla 2008 i 2010, także w wersji Express); 
  • szablony składników projektu (Item Templates) - pozwalają na łatwe dodanie składników takich jak nowy widok lub ViewModel
  • snippety - zbiór fragmentów kodu pozwalających wygenerować składniki narzędzia takie jak Dependency Property czy Locator Property

Przed rozpakowaniem warto pamiętać o odblokowaniu archiwów (przynajmniej w Winows 7, nie wiem jak w innych systemach), opcja dostępna we właściwościach pliku. 
Ogólnie zasada jest prosta: pliki szablonów rozpakowuje do katalogu szablonów, a snippety do katalogu snippetów :).  Ich położenie można znaleźć w ustawieniach Visual Studio. Dla szablonów sprawdzamy w Tools -> Options -> Project and Solutions, natomiast dla snippetów Tools -> Code Snippet Manager -> My Code Snippets. Po rozpakowaniu i restarcie Visual Studio możemy cieszyć się nowymi elementami.

Przykładowy projekcik
Aby przedstawić jak tworzy się programy z wykorzystaniem MVVM Light Toolkit, napiszę małą gierkę w "Zgadywanie liczby". Jedno z pierwszych zadań programistycznych, gdy uczymy się nowego języka. Program losuje jakąś liczbę z zadanego przedziału, a użytkownik ma ją odgadnąć z pomocą wskazówek typu "za dużo" lub "za mało"
Zacznę od zwykłego projektu WPF Application, żeby pokazać z jakich składników budujemy aplikację opartą na MVVMLT, takie podejście ma również zastosowanie, gdy chcemy przystosować do stosowania narzędzia już istniejący projekt. Oczywiście w normalnym przypadku po prostu stworzyłbym projekt z gotowego szablonu. Na samym początku warto dodać referencję do biblioteki GalaSoft.MvvmLight.WPF4.dll.

ViewModel
Zaczynam od stworzenia obiektu ExampleViewModel, który będzie powiązany z głównym widokiem, dla wygody i porządku umieszczam go w katalogu ViewModel. Jest to prosta klasa dziedzicząca po ViewModelBase, w której to autor toolkita zawarł wiele wspomagaczy. Ta klasa będzie odpowiedzialna za przechowywanie danych i odpowiednie reagowanie na zachowania użytkownika. 
Dodam właściwość SecretNumber typu int, reprezentującą liczbę, którą należy odgadnąć Suggestion typu string, w której zapisywane będą sugestie odnośnie wybranej liczby oraz dwie właściowści typu bool IsInProgress oraz IsSolved, przedstawiające czy aktualnie użytkownik jest w trakcie zgadywania oraz czy liczba została odgadnięta (posłużą one do pokazywania i ukrywania odpowiednich komunikatów). We wszystkich podczas ustawiania wartości wywołam funkcję RaisePropertyChanged() z klasy bazowej, opakowuje ona funkcjonalność interfejsu INotifyPropertyChanged. Dzięki temu będzie możliwe skorzystanie z dobrodziejstw Data Bindingu i przy każdej zmianie wartości zmiennej zarówno w kodzie, jak i przez użytkownika, jej wartość będzie na bieżąco aktualizowana. Aby nie wpisywać wszystkiego "z palca" najlepiej jest skorzystać ze snippetu mvvminpc
#region SecretNumber Property
public const string SecretNumberPropertyName = "SecretNumber";
private int _secretNumber = 0;

public int SecretNumber
{
 get
 {
  return _secretNumber;
 }

 set
 {
  if (_secretNumber == value)
   return;

  _secretNumber = value;

  RaisePropertyChanged(SecretNumberPropertyName);
 }
} 
#endregion

#region Suggestion Property
public const string SuggestionPropertyName = "Suggestion";
private string _suggestion = "";

public string Suggestion
{
 get
 {
  return _suggestion;
 }

 set
 {
  if (_suggestion == value)
   return;

  _suggestion = value;

  RaisePropertyChanged(SuggestionPropertyName);
 }
}
#endregion

#region IsInProgress Property
public const string IsInProgressPropertyName = "IsInProgress";
private bool _isInProgress = false;

public bool IsInProgress
{
 get
 {
  return _isInProgress;
 }

 set
 {
  if (_isInProgress == value)
   return;

  _isInProgress = value;

  RaisePropertyChanged(IsInProgressPropertyName);
 }
}
#endregion

#region IsSolved
public const string IsSolvedPropertyName = "IsSolved";
private bool _isSolved = false;

public bool IsSolved
{
 get
 {
  return _isSolved;
 }

 set
 {
  if (_isSolved == value)
   return;

  _isSolved = value;

  RaisePropertyChanged(IsSolvedPropertyName);
 }
}
#endregion
ViewModel Locator
W MVVM Light Toolkit klasa ViewModel Locator odpowiada za przetrzymywanie referencji do wszystkich obiektów ViewModel wykorzystywanych w projekcie. Dzięki takiemu zabiegowi można łatwo tworzyć dowiązania do właściwości i komend w różnych obiektach ViewModel. Pozwala to także w Expression Blend stworzyć Object Data Source wskazujący właśnie na tę klasę i wszystkie bindingi tworzyć przez wygodny interfejs lub proste drag&drop. 
Tym razem również można skorzystać z przygotowanych szablonów pochodzących z paczki MVVM Light Toolkit. Również do katalogu ViewModel dodajemy nową pozycję MVVMViewModelLocator (WPF), a następnie w ciele klasy korzystamy ze snippetu mvvmlocatorproperty. W ramach pól snippetu podajemy nazwę klasy będącej powiązaniem między widokiem a modelem (ExampleViewModel) oraz nazwę właściwości jaka ma być dostępna na zewnątrz (Example). Po tych czynnościach budujemy projekt. 
Aby powiązać obiekt ViewModel Locator z naszą aplikacją najlepiej dodać ją jako zasób w pliku App.xaml, poprzez to będzie on dostępny w całej aplikacji, a poszczególne obiekty ViewModel będzie można przypisać jako Data Context bezpośrednio w widoku odwołując się do głównego Locatora. Można to zrobić na dwa sposoby: wpisać "z palca" w kodzie lub wykorzystać wygodny interfejs Expression Blend. Jeśli mamy do dyspozycji tylko Visual Studio w pliku App.xaml w tagu <Application.Resources>
 

pamiętając jednocześnie o wprowadzeniu nowej przestrzeni nazw:
xmlns:vm="clr-namespace:MVVMLightToolkit.ViewModel"
Teraz w każdym widoku możemy zbindować odpowiedni obiekt ViewModel:
 
  
Jeśli obiekt nie ustawi własnego DataContext, to jest on dziedziczony z kontrolki nadrzędnej przez co ustawiając go dla obiektu Window zapewniamy ten sam kontekst dla wszystkich potomków.
Aby uzyskać taki sam efekt korzystając z Blend'a, w zakładce Data, wybieramy Create Object Data Source i nadajemy mu nazwę Locator. 
Następnie możemy już przeciągnąć z listy obiekt Example, reprezentujący nasz aktualny obiekt ViewModel, na przykład na obiekt Window.

Tworzenie komend
Na początek stworzymy dwie komendy: Start, która wylosuje nową liczbę do odgadnięcia i jednocześnie ukryje lub pokaże odpowiednie kontrolki, oraz CheckGuess, która będzie miała za zadanie sprawdzenie czy podana przez użytkownika liczba jest prawidłowa i ewentualne podanie wskazówek.
W MVVM Ligth Toolkit za reprezentowanie komend odpowiada klasa RelayCommand, która opakowuje interfejs ICommand, dając jednocześnie wygodny sposób tworzenia nowych komend, bez potrzeby zagłębiania się w implementację. Klasa ta w konstruktorze oczekuje obiektu typu Action, czyli prościej mówiąc delegacji która nie przyjmuje parametrów oraz nic nie zwraca, który będzie odpowiadał funkcji Execute() z interfejsu ICommand. Metoda ta jest wykonywana w momencie wywołania komendy. Można również podać drugi argument będący obiektem typu Predicate (delegacja nie przyjmująca parametrów i zwracająca bool), który będzie implementował metodę CanExecute(), określa ona czy spełnione są warunki, aby móc wykonać daną komendę. Istnieje również wersja generyczna obiektu RelayCommand, pozwala ona na przekazywanie parametru do komendy.
Do implementacji prostych komend można skorzystać z wyrażeń lambda:
#region Start
private RelayCommand _startCommand;

public RelayCommand Start
{
 get {
  if (_startCommand == null)
  {
   _startCommand = new RelayCommand(
    () =>
    {
     SecretNumber = rand.Next(10) + 1;
     IsSolved = false;
     IsInProgress = true;
     Suggestion = "";
    }
   );
  }
  return _startCommand; }
}

#endregion

#region CheckGuess Command
private RelayCommand<int> _checkGuess;

public RelayCommand<int> CheckGuess
{
 get {
  if (_checkGuess == null)
   _checkGuess = new RelayCommand<int>(
    x =>
    {
     if (x == _secretNumber)
     {
      IsInProgress = false;
      IsSolved = true;
     }
     else if (x < _secretNumber)
      Suggestion = "to low";
     else
      Suggestion = "to high";
    });

  return _checkGuess; 
 }
}
#endregion
Komenda Start, jedynie losuje nową sekretną liczbę i ustawia właściwości określające stan zabawy na odpowiednie wartości logiczne. W komendzie CheckGuess, wykorzystujemy przekazany parametr do sprawdzenia czy użytkownik zgadł ukrytą liczbę, jest ona przekazywana poprzez binding z kontrolki.

Interfejs
Na potrzeby przykładu stworzyłem jak najprostszy interfejs użytkownika pozwalający na wprowadzenie danych i interakcję. 
Uproszczony kod XAML, który go generuje:

 
  
  
 
 
 
  
   
   
  
  
  
 


Tworzenie bindingów
Najciekawszą czynnością jest utworzenie odpowiednich powiązań pomiędzy widokiem i obiektem ViewModel. Niektóre są naprawdę proste i nie wymagają większego komentarza, gdyż jedynie wiążą jedną właściwość lub komendę, jak na przykład w przypadku przycisku rozpoczynającego grę:

To samo jest z polami tekstowymi txblSecret oraz txblSugestion, powiązanymi odpowiednio z właściwościami SecretNumber oraz Sugestion.
Nieco bardziej skomplikowane jest powiązanie przycisku odpowiedzialnego za sprawdznie czy użytkownik podał właściwą liczbę:
Binding ustawia komendę wykonywaną w momencie wciśnięcia przycisku na CheckGuess, jako jej parametr podaje właściwość Text, pola tekstowego txbUserGuess. Jednak komenda CheckGuess przyjmuje jako parametr obiekt typu int, a właściwość Text jest stringiem, dlatego też musi nastąpić konwersja pomiędzy oba typami. Do tego celu stworzyłem prosty obiekt StringToIntConverter, który dokonuje takiej zamiany w obie strony. Dziedziczy on po interfejsie IValueConverter i implementuje jego dwie metody: Convert oraz ConvertBack:
[ValueConversion(typeof(string), typeof(int))]
class StringToIntConverter : IValueConverter
{
 #region IValueConverter Members

 public object Convert(object value, Type targetType,
   object parameter, System.Globalization.CultureInfo culture)
 {
  string str = value.ToString();

  if (string.IsNullOrEmpty(str))
   return 0;
  else
   try
   {
    int result = 0;

    int.TryParse(str, out result);
    return result;
   }
   catch (ArgumentException)
   {
    return 0;
   }
 }

 public object ConvertBack(object value, Type targetType,
   object parameter, System.Globalization.CultureInfo culture)
 {
  return value.ToString();
 }

 #endregion
}
Konwerter ten dodajemy jako zasób obecnego widoku:

Dodatkowo, żeby nie zdradzać użytkownikowi naszej sekretnej liczby, należy ukryć w odpowiedni sposób oba panele. Można tego dokonać poprzez odpowiednie powiązanie ich właściwości Visibility z właściwościami z naszego obiektu ViewModel: IsInProgress oraz IsSolved:
//dla panelu pierwszego

//dla panelu drugiego

Korzystamy przy tym z konwertera BooleanToVisibilityConverter dostępnego domyślnie.

Podsumowanie
Na tym małym przykładzie (choć tekst rozrósł się bardziej niż przypuszczałem) można zobaczyć jak wykorzystać podstawowe składniki MVVM Light Toolkit, czyli właściwości implementujące INotifyPropertyChanged, komendy (zarówno z parametrami jak i bez) oraz ViewModel Locator. W następnym poście przedstawię jeszcze obiekt Messenger do komunikacji między składowymi aplikacji oraz EventToCommand, pozwalający na podpięcie w łatwy sposób dowolnych zdarzeń do komend.

Źródła tego przykładu można pobrać stąd.

czwartek, 16 września 2010

Frustrujący MVVM

Od dawna nosiłem się z zamiarem poznania wzorca MVVM (Model-View-ViewModel), na którym opiera się WPF, jednak im głębiej go poznawałem tym bardziej wydawał mi się on irytujący. Problem z MVVM jest taki, że jest to tylko wzorzec, opisujący jedynie podstawy działania i przedstawiający założenia do ogólnej koncepcji jaką należy przyjąć w projekcie. W teorii wygląda to bardzo ładnie, oddzielenie interfejsu od logiki biznesowej miały pozwolić na rozdzielenie pracy programisty i designera. Zachowujemy porządek w projekcie, nie tworzymy żadnego kodu w tak zwanym Code Behind (to znaczy w plikach *.xaml.cs), który nie wykonuje się podczas edycji w Expression Blend.
Z drugiej strony spełnienie wszystkich wymagań dotyczących wzorca, implementowanie wymaganych składników jest niesłychanie nudne, wtórne i podatne na błędy. Gdy patrzyłem na te dziesiątki linii podobnego do siebie kodu zastanawiałem się, dlaczego tak utrudniono życie programistom? Przecież to niepodobne do twórców .NET Framework'a, do tej pory większość zmian ułatwiała życie i sprawiała, że kodowanie stawało się przyjemniejsze. Poza tym nie istnieją jednoznaczne wytyczne jak należy implementować poszczególne wymagania. Każdy programista udzielający porad w kwestiach MVVM ma swoje zdanie na ten temat, strasznie trudno znaleźć uniwersalne porady, które nie wymagałyby przepisania części naszej aplikacji w celu dostosowania do przykładów.
Dlatego też zacząłem przyglądać się różnym toolkitom (czy może po prostu narzędziom), które ponoć miały ułatwiać pracę z WPF i MVVM, jak sugerowano w tym temacie na StackOverflow. To co znalazłem zaczęło mnie przytłaczać natłokiem informacji koniecznych do przyswojenia przed rozpoczęciem pracy, ogromem możliwości, których pewnie i tak bym nie wykorzystał oraz przykładowymi aplikacjami, które zawierały całą masę funkcjonalności, jednak tak na prawdę nie pokazywały jak zacząć pracę z frameworkiem. Przypatrywałem się między innymi Prismowi, Cinchowi i MVVM Foundation.
Zainteresowałem się także MVVM Light Toolkit mając nadzieję, że nazwa nie jest bezpodstawna. Zacząłem od obejrzenia prezentacji autora z konferencji MIX10, przedstawiającej jego narzędzie. To co zobaczyłem całkowicie mnie przekonało. Dzięki niemu tworzenie aplikacji opartych o MVVM przestaje być monotonne (jak reklamuje sam autor "MVVM Light Toolkit is taking out some of the monotony of implementing MVVM"), a dodatkowo zyskujemy kilka możliwości trudno dostępnych z poziomu "gołego" wzorca (takich jak Messaging, czyli możliwość łatwego komunikowania między różnymi częściami aplikacji albo EventToCommand, pozwalającego na podpinanie dowolnych zdarzeń dostępnych w kontrolkach do obiektów typu Command). MVVM Toolkit skupia się także na tak zwanym Blendability, czyli możliwości łatwego tworzenia programu niezależnie przez programistę (na przykład w Visual Studio) oraz designera (oczywiście w Expression Blend).

Na chwilę obecną zdecydowałem się właśnie na MVVM Light Toolkit i zabieram się za dokładniejsze jego poznanie. Niedługo na pewno pojawi się notka z podstaw jego wykorzystania i wyjaśniająca niektóre z użytych w tym poście terminów.

niedziela, 12 września 2010

[Narzędziownia] CodeRush Express

CodeRush Express jest narzędziem, które niesamowicie ułatwia mi pracę z kodem. Co prawda jest to tylko darmowa wersja potężnego programu jakim jest pełna wersja CodeRush i zawiera masę ograniczeń oraz nie posiada wielu funkcjonalności. Jest zaledwie odbiciem tego co można uzyskać po zapłaceniu całkiem sporej kwoty (od 249$). Pomimo tego po krótkim okresie przyzwyczajenia daje całkiem sporego boosta podczas pisania.
Niestety w wersji dla Visual Studio 2010 cześć już i tak ograniczonych funkcjonalności została jeszcze bardziej obcięta , gdyż niektóre operacje przejęło samo Visual Studio. Wprowadza to trochę zamieszania i niejednolity interfejs współpracy z programistą; oczywiście w wersji płatnej wszystkie te operacje wykonywane są przez CodeRush. Te wyłączone z Visual Studio 2010 opcje ciągle działają prawidłowo w wersji 2008, dlatego też praca w starszej wersji jest nawet wygodniejsza. O ironio.
Dużą część funkcji wywołuje się za pomocą "magicznego skrótu", domyślnie jest to Ctrl+~ (tylda). Dostępne opcje będą się zmieniać w zależności od kontekstu z jakiego jest ten skrót wywoływany (miejsce umieszczenia kursora lub zaznaczony fragment kodu).
Poniżej kilka z ułatwień, z których korzystam podczas normalnej pracy. Jest i tak tylko część z dostępnych w darmowej wersji, na poznanie ich wszystkich trzeba poświęcić trochę więcej czasu, może kiedyś uda mi się opanować ich trochę więcej.

Deklarowanie zmiennych bez znajomości typu
Nie chodzi mi bynajmniej o słowo kluczowe var, chodzi o odwrotny niż zwykle sposób deklarowanie zmiennych. Jeśli nie pamiętamy jaki typ zwraca funkcja najpierw wywołujemy funkcję, potem wciskamy magiczny skrót i wybieramy Declare local, CodeRush sam rozpozna typ i pozostawi zaznaczenie na nowej nazwie zmiennej. Doskonale sprawdza się to w połączeniu z funkcjami o oczywistych funkcjach, na przykład wykonanie powyższych czynności dla funkcji GetSession() zaproponuje nam nazwę session, więc nie pozostanie nic innego jak tylko wdusić Enter.
Deklarowanie nieistniejących jeszcze funkcji
Przydatne jeśli właśnie wymyśliliśmy, że jest nam potrzebna nowa metoda/właściwość/event, której jeszcze nigdzie nie ma. Możemy wpisać w kodzie wywowałnie tej funkcji wraz z parameterami, a po naciśnięciu magicznego skrótu zostanie zadeklarowana nowa funkcja z już odpowiednio ustawionymi parametrami i typem zwracanym.

Rozdzielanie deklaracji od inicjalizacji
Zaznaczjąc typ zmiennej i jej nazwę w linijce z deklaracją możemy z linijki:
TreeViewItem item = new TreeViewItem();
otrzymać:
TreeViewItem item;
item = new TreeViewItem();
a następnie poszerzyć zakres zmiennej lub wypromować na składową klasy, albo też skorzystać z następnej możliwości. Możemy też łatwo przenieść inicjalizację w pobliże pierwszego wykorzystanie zmiennej.

Wprowadzanie stałych z już istniejących wartości
Jeśli użyliśmy tymczasowej wartości wpisanej na sztywno (na przykład stringa lub jakiejś liczby) to możemy łatwo podmienić je na stałe (wraz ze wszystkimi wystąpieniami w danym zakresie). Można także wyeksportować takie wyrażenia do zasobów (Resources).
Przenoszenie bloku tekstu w górę lub w dół
Za pomocą skrótu Alt+[strzałka w górę] lub Alt+[strzałka w dół] przenosimy linijkę lub zaznaczony blok tekstu o jedną linijkę w górę lub w dół. Zamiast zaznaczać linijkę wycinać ją, przenosić kursor wyżej, robić linijkę odstępu i tam wklejać. Szybciej i bardziej intuicyjnie.

Tworzenie metody z zaznaczonego tekstu
Najbardziej spektakularna funkcjonalność. Zaznaczamy fragment kodu, naciskamy magiczny skrót i na ekranie pojawia się kolorowe podsumowanie formy nowej funkcji (parametry, typy zwracane). Po zaakceptowaniu i wybraniu nowej nazwy cieszymy się nową funkcją z już gotowym kodem.

Operacje na stringach
Zamiast maszkar w stylu:
string tmp = "Name: " + person.Name + ", Age: " + person.Age.ToString() + ".";
po naciśnięciu magicznego skrótu otrzymujemy:
string tmp = String.Format("Name: {0}, Age: {1}.", person.Name, person.Age);
To samo tyczy się przekształcania wielolinijkowego łączenia stringów w jeden za pomocą StringBuildera (jak wiadomo jest to o wiele bardziej wydajna metoda):
string longString = person.GetName();
longString += person.CalculateAge() + Environment.NewLine;
longString += Greetings.Hello();
dostajemy:
StringBuilder longStringBuilder = new StringBuilder(person.GetName());
longStringBuilder.Append(person.CalculateAge() + Environment.NewLine);
longStringBuilder.Append(Greetings.Hello());
Dodawanie kontraktów do metody
Czasem na początku metody należy sprawdzić czy przekazane do niej wartości są na tyle poprawne, że można kontynuować wykonywanie metody. CodeRush udostępnia trzy metody wygenerowania kontraktów na początku metody :
  • rzucenie wyjątkiem
  • wykorzystanie asercji 
  • wyjście z metody
Nawigacja wewnątrz wielbłądzich nazw
Nie wiedziałem jak inaczej przetłumaczyć konwencję nazewnictwa Camel Case. Ogólnie chodzi o to, że pomiędzy składowymi nazwy możemy poruszać się przez naciśniecie kombinacji Alt+[strzałki w bok]. Umożliwia to szybsze zaznaczanie fragmentów nazw lub zmianę nazwy funkcji.
Poszerzanie lub zawężanie obszaru zaznaczenia
Dzięki klawiszom Num+ i Num- można łatwo zaznaczać obszary tekstu bez udziału myszki. Jedno wciśnięcie plusa pozwala zaznaczyć całe aktualne wyrażenie, drugie całą linijkę, następne cały aktualny blok i tak dalej. Minus kolejno redukuje zaznaczenie. 
Duplikowanie linii
Po prostu wciskamy Shift+Enter i bieżąca linia zostaje zduplikowana pod spodem. Prosta funkcjonalność, ale przydatna, podczas deklaracji wielu pól tego samego typu.

Usuwanie niepotrzebnych nawiasów klamrowych
Z przyzwyczajenia objąłeś ciało if'a lub for'a klamrami, a ma on tylko jedną linijkę? Nie szkodzi, wystarczy ustawić kursor na nawiasie i nacisnąć magiczny skrót, a nawiasy znikają.

Co jeszcze?
Wymienione powyżej możliwości to i tak tylko część dostępnych funkcjonalności. Można jeszcze wykonywać całą masę operacji, których albo jeszcze nie odkryłem albo nie miałem okazji zastosować. Zainteresowanych odsyłam do artykułu opisującego możliwości darmowej wersji CodeRush'a. Jest tam tego naprawdę dużo, aż trudno wszystko zapamiętać.

Czego brakuje
Najbardziej brakuje mi możliwości używania w kodzie szablonów (templates). Jest to coś podobnego do snippetów z Visual Studio, jednak mają większe możliwości i jest bardzo dużo. Dostosowują się one do kontekstu w jakim są wywoływane. Na przykład w jeśli w miejscu, w którym możemy zadeklarować metodę wpiszemy:
ml.
i naciśniemy Spację zostanie zadeklarowana metoda zwracająca generyczną listę z polami do edycji typu oraz nazwy funkcji. 
Równie przydatna może być możliwość uruchamiania pojedynczych testów jednostkowych z poziomu edytora.
Poza tym nie jest dostępna spora ilość z opcji refactoringu, jak na przykład przeniesienie klasy do osobnego pliku, czy zmiana dostępności metody jednym skrótem.

Podsumowanie
Pluginy zwiększające produktywność programisty są niewątpliwie niesamowicie przydatne i nie na darmo noszą taką nazwę. Pełne wersje takich programów wykorzystuje i zachwala wielu programistów. Na przykład Procent w serii postów pod niebiosa wychwala konkurencję dla CodeRush, czyli Resharpera.
Zdecydowanie polecam zainstalowanie darmowego CodeRush Express i samodzielne wypróbowanie jego możliwości. Osobiście nie stać mnie na zakup pełnej wersji, ale jestem ciekaw czy istnieją jakieś darmowe alternatywy dla tego pluginu.

piątek, 3 września 2010

Klasa dostępu do danych

To ostatni, przynajmniej na razie post o NHibernate. Jest on podsumowaniem wszystkich moich wysiłków w kierunku poznania podstaw tego ORMa. 
Wcześniejsze etapy prac przedstawiłem w postach o mapowaniach NHibernate'a oraz zarządzaniu sesjami.


Zmiany w klasie SessionManager
W porównaniu do klasy przedstawionej w poprzednim poście musiałem wprowadzić niewielkie zmiany. Zdecydowałem się jednak zrobić ją klasą statyczną oraz przeniosłem funkcję MakeTransaction do głównej klasy dostępu do danych, oto jej aktualny kod:
public static class SessionManager
{
 private static ISessionFactory sessionFactory;
 private static object syncObj = new object();
 private static ISessionFactory SessionFactory
 {
  get
  {
   if (sessionFactory == null)
    lock (syncObj)
    {
     if (sessionFactory == null)
     {
      sessionFactory = (new NHibernate.Cfg.Configuration())
       .Configure().BuildSessionFactory();
     }
    }

   return sessionFactory;
  }
 }

 public static ISession GetSession()
 {
  return SessionFactory.OpenSession();
 }
}
Zmiany zostały spowodowane małą reorganizacją podejścia do sposobu korzystania z klasy DataProvider. Początkowo miała ona mieć tylko jedną instancję dla całego programu i posiadać obiekt klasy SessionManager, jednak takie działania powodowały błędy sesji. Dlatego zdecydowałem się napisać coś podobnego do kodu, który widziałem w Summer of NHibernate.


Klasa DataProvider
Klasa zawiera jedną składową prywatną i jest nią obiekt implementujący interfejs ISession. Każdy obiekt dostępu do danych posiada własną sesję, na której operuje. Podczas tworzenia można przekazać obiekt sesji jako parametr lub w domyślnym konstruktorze uzyskać sesję z obiektu SessionManager (za pomocą statycznej funkcji GetSession()). Dodatkowo implementuje ona interfejs IDisposable, dzięki czemu można jej będzie używać w połączeniu z instrukcją using; jedyne co robi funkcja Dispose() to posprzątanie po sesji. 
private ISession session;
public DataProvider()
{
 session = SessionManager.GetSession();
}

public DataProvider(ISession _session)
{
 session = _session;
}

public void Dispose()
{
 if (session != null)
 {
  session.Dispose();
 }
}
Do tej klasy trafiła również z klasy SessionManager funkcja MakeTransaction, odpowiedzialna za wykonywanie w wygodny i bezpieczny sposób operacji modyfikujących stan bazy danych. Różni się od poprzedniej wersji jedynie tym, że sesja jest już składową klasy dlatego nie ma potrzeby jej tworzyć.
private void makeTransaction(Action<ISession> operation)
{
 using (var tx = session.BeginTransaction())
 {
  try
  {
   operation(session);

   tx.Commit();
  }
  catch (NHibernate.HibernateException)
  {
   tx.Rollback();
   throw;
  }
 }
}
Aby nie pisać wtórnych i powtarzających się metod podstawowych operacji (dodawania, usuwania, aktualizowania i pobierania obiektu po ID) dla każdej klasy osobno, napisałem cztery metody generyczne, pozwalające na wykorzystanie z dowolnym typem poprawnym dla mojej bazy.
T GetById(int id)
void Add(T obj)
void Delete(T obj)
void Update(T obj) 
Oprócz tego w miarę postępów projektu i pojawiania się nowych wymagań do obiektu dostępu do danych, będę pisał nowe funkcje pobierające specyficzne dane z bazy. Na razie w ramach zasady YAGNI nie zaprzątam sobie tym głowy.
Typowy sposób pracy z obiektem DataProvider może wyglądać następująco:
using (DataProvider dp = new DataProvider())
{
 Album album = dp.GetById<album>(1) as Album;
 album.Name = "New Name";

 dp.Update<album>(album);
}
Dzięki wykorzystaniu instrukcji using, po opuszczeniu bloku sesja obiektu jest uwalniana. Nowy obiekt DataProvider można tworzyć w każdym miejscu w programie, w którym jest potrzebny
Do tego napisałem zestaw kilku podstawowych testów sprawdzających każdą napisaną funkcję, wszystkie jak na razie przechodzą bez błędów. W tym momencie pracuję na testowej bazie, z nic nie znaczącymi danymi. Jednak, gdy nadejdzie odpowiedni moment będę musiał rozdzielić wyraźnie ją na bazę danych faktycznego programu i bazę danych do testów, ale w tym przypadku również traktuję to jako problem, którym nie muszę się na razie zajmować.


Krótkie podsumowanie prac z NHibernate
Opanowanie podstaw NHibernate'a zajęło mi znacznie więcej czasu niż początkowo na to planowałem. Nie do końca ze względu na to, że były one bardzo trudne. Przede wszystkim wpyw miał brak czasu na poświęcenie się nauce przez dłuższą chwilę i ciągłe "rozdrobnienie" na masę innych czynności. Poza tym Summer of NHibernate, pomimo iż świetnie zrobione i bardzo dobrze wytłumaczone krok po kroku, nie nadaje się do szybkiego opanowania materiału. Każdy odcinek ma sporo ponad godzinę, a czas na oglądanie nie był jedynym spędzonym przeze mnie nad kursem. Następną nową wiedzę będę zdobywał z tutoriali lub wprost z dokumentacji, mam nadzieję, że będzie szybciej.
Na razie poznałem znikomy ułamek możliwości NHibernate'a, ale to co zobaczyłem przekonuje mnie do tego, że warto z niego korzystać (lub z innego ORMa) oraz że prawdziwe jest zdanie "Pisząc ręcznie dostęp do danych OKRADAMY pracodawcę/klienta". Niezbędne podstawy opanowałem, a jeśli kiedyś przyjdzie potrzeba wykorzystania zaawansowanych funkcjonalności będę gotów jej sprostać.

środa, 1 września 2010

Zarządzanie sesjami w NHibernate

Sesje w NHibernate są kluczowymi obiektami, spośród wszystkich klas NHibernate'a to właśnie do nich najczęściej się odwołujemy. Od tworzenia i zwalniania sesji zależy prawidłowe działanie programu. Dlatego też istotne jest, aby odpowiednio wszystko poukładać, a jeszcze dobrze by było, aby korzystanie z NHibernate'a nie straciło przez to na wygodzie.

O co się rozchodzi?
W NHibernate podstawową "jednostką pracy" (Unit of work) jest obiekt implementującym interfejs ISession. To z jego poziomu możemy wykonywać szereg operacji powiązanych z bazą danych (pobieranie, dodawanie, edytowanie lub usuwanie danych, ogólnie CRUD), by na końcu wydać komendę zaktualizowania faktycznego stanu bazy. Obiekt sesji samodzielnie śledzi zmiany obiektów, wie które są nowo dodane, które zmodyfikowane i tak dalej. Dopiero podczas wywoływania funkcji Flush, za jednym zamachem wszystkie te zmiany zostają wprowadzone do faktycznej bazy.
Obiekt ISession możemy uzyskać z obiektu ISessionFactory przez wywołanie funkcji OpenSession(). Ten znów tworzony jest podczas konfigurowania NHibernate'a, która może być dość kosztowna obliczeniowo i nie ma konieczności przeprowadzania jej więcej niż jeden raz. Dlatego też popularnym podejściem jest przechowywanie tylko jednego obiektu ISessionFactory dla całej aplikacji i tworzenie za jego pomocą obiektów sesji w odpowiednich momentach. 
Są różne podejścia do tego jak często powinno się tworzyć obiekty sesji, na przykład jedna sesja dla całej aplikacji lub sesja dla każdego wątku. Od razu odrzuciłem możliwość korzystania z jednej sesji w całej aplikacji (jeśli wystąpi jakikolwiek wyjątek podczas pracy z sesją, nawet obsłużony przez nas, to sesja przestaje być stuprocentowo wiarygodna) i zdecydowałem się podzielić operacje CRUD na dwie grupy: modyfkujące dane (UPDATE, DELETE, INSERT) oraz jedynie pobierające dane (SELECT). Taki podział sugeruje użycie zasady Command/Query Separation, dlatego też dla obu zastosuję nieco inne podejście:
  • grupa modyfikująca (Command): podczas modyfikacji danych może zajść sporo nieprzewidzianych okoliczności: dane mogą być niezgodne ze stawianymi wymaganiami, ktoś inny może właśnie modyfikować rekord, albo może on już w ogóle nie istnieć. Dlatego dobrze jest skorzystać z transakcji, wtedy jeśli cokolwiek podczas modyfikowania danych pójdzie nie tak, to istnieje możliwość cofnięcia wszystkich zmian wprowadzonych od początku transakcji. Jednym słowem albo wszystko pójdzie dobrze albo nic. Trochę o transakcjach w następnym poście
  • grupa pobierająca (Query): w tym przypadku istotniejsze jest, aby móc wykorzystać potencjał mechanizmu Lazy Loadingu. Co to takiego właściwie jest? Już wyjaśniam.

Lazy Loading
W NHibernate Lazy Loading pozwala na pobieranie z bazy niektórych danych dopiero w momencie, gdy będą one potrzebne, w niektórych przypadkach pozwala to zaoszczędzić czas i zasoby oraz nie popaść w nieskończone zagnieżdzenie obiektów. Jako przykład weźmy fragment schematu bazy danych, którą mam zamiar wykorzystać w projekcie:

Obiekt klasy Album zawiera zestaw (ISet<>) obiektów klasy Photo. Każdy obiekt klasy Photo zawiera obiekt klasy Album do którego należy, który z kolei zawiera zestaw obiektów klasy Photo. I tak dalej aż po granice pamięci. Dzięki mechanizmowi "opóźnionego ładowania" (jeśli mogę to tak przetłumaczyć) taka niekorzystna sytuacja nie ma miejsca. Dane z obcych tabel są pobierane dopiero w momencie, kiedy mają zostać wykorzystane. Jeśli pobierzemy z bazy obiekt klasy Album i nie skorzystamy z jego własności Photos to lista zdjęć nie zostanie faktycznie pobrana. Oczywiście z pozostałych własności możemy korzystać do woli. W momenci pierwszego odwołania do Photos, zdjęcia należące do albumu zostają pobrane z bazy. Takie podejście byłoby szczególnie korzystne jeśli mielibyśmy bardzo dużo zdjęć w albumie a chcieli pobrać jedynie jego opis. 
Jednak żeby skorzystać z tego mechanizmu, sesja nie może być zamknięta przed skończeniem pracy z pobranym obiektem. Dlatego też, w tym przypadku nie można skorzystać z dyrektywy using. Co prawda jest ona bardzo wygodna, gdyż automatycznie zamyka sesję i zwalnia jej zasoby, jednak uniemożliwia załadowanie potrzebnych danych po wyjściu z funkcji. 

Moja implementacja
Odpowiedzialność za zarządzanie sesją spada na na obiekt SessionManager i będzie on wewnętrzną klasą data providera, dzięki temu inna klasa nie będzie nam grzebać w sesjach. Przechowuje on instancję obiektu ISessionFactory, tworzoną jedynie raz, w momencie pierwszego odwołania. Dostęp do niego zapewnia prywatna właściwość:
private ISessionFactory sessionFactory;
private ISessionFactory SessionFactory
{
 get
 {
    if (sessionFactory == null)
      lock (syncObj)
        if (sessionFactory == null)
          sessionFactory = (new NHibernate.Cfg.Configuration())
          .Configure().BuildSessionFactory();

  return sessionFactory;
 }
}
Na potrzeby wszystkich metod z grupy Query, dostępna jest funkcja zwracająca obiekt ISession, będący jedynie obudową dla ISessionFactory.OpenSession():
public ISession GetSession()
{
 return SessionFactory.OpenSession();
}
Najciekawsza jest funkcja przeznaczona dla metod typu Command. Opakowuje cały schemat wykonania transakcji w NHibernate, to znaczy:
  • zdobądź obiekt sesji
  • rozpocznij transakcję, czego wynikiem jest obiekt ITransaction
  • wykonaj na obiekcie sesji wszystkie składowe transakcji, łącznie z oczyszczeniem sesji (flush)
  • potwierdź transakcję do wykonania (commit)
  • w razie problemów wykonaj przywrócenie stanu bazy (rollback) do tego sprzed rozpoczęcia transakcji
  • na końcu zwolnij wszystkie wykorzystane zasoby
Tę sprytną funkcję zapożyczyłem od Procenta:
public void MakeTransaction(Action operation)
{
 using (var session = GetSession())
 {
  using (var tx = session.BeginTransaction())
  {
   try
   {
    operation(session);

    tx.Commit();
   }
   catch (NHibernate.HibernateException)
   {
    tx.Rollback();
    throw;
   }
  }
 }
}
Funkcja najpierw wykonuje standardowe przygotowania transakcji, następnie przekazuje sterowanie obiektem sesji delegatowi i w razie potrzeby obsługuje błędy i rzuca wyjątkiem. Wykorzystać ją można na przykład tak:
sessionManager.MakeTransaction(
 session =>;
 {
  session.Save(obj);
 });
Takie podejście jest niesłychanie wygodne i oszczędza sporo powtarzającego się kodu.

To w zasadzie tyle jeśli chodzi o moją implementację obiektu zarządzającego sesjami. W następnym poście opiszę całą klasę dostępu do danych i to będzie na razie koniec postów o NHibernate. Całość można już teraz podejrzeć na BitBucket.