Tworzenie bootstrappera aplikacji przy użyciu CaliburnMicro

Witam po długiej przerwie. W dzisiejszym wpisie postaram się krótko opisać w jaki sposób stworzyć bootstrapper aplikacji przy użyciu CaliburnMicro. Ponadto przedstawię w jaki sposób skonfigurować bootstrapper w taki sposób, aby Caliburn wykorzystywał nasz własny kontener IOC.

1. Wstęp

CaliburnMicro jest to framework MVVM, który w znacznym stopniu ułatwia i przyśpiesza pisanie aplikacji pod WPF,Silverlight,WindowsPhone oraz WinRT. Zdecydowałem się poznać ten framework z uwagi na jego przenośność na różne platformy. Wcześniej całkiem sporo czasu poświęciłem Prismowi, jednakże z powodu licznych problemów z jego działaniem pod WindowsPhonem chciałem spróbować czegoś nowego.

2. Bootstrapper – Silverlight

Bootstrapper jest to ogólnie rzecz biorąc klasa inicjalizująca całą aplikację. W trakcie odpalania bootstrappera na ogół konfiguruje się kontener IOC(rejestruje wszystkie potrzebne typu), inicjalizuje się połączenie z serverem, oraz odpala się główne okno aplikacji – tzw. Shella. Dlatego też musimy odchudzić plik App.xaml.cs i zostawić w nim jedynie konstruktor wraz z wywołaniem funkcji InitializeComponents()

Zacznijmy od utworzenia nowego projektu typu SilverlightApplication, do którego dodajemy referencje do dll-ek Cliburna. Możemy to zrobić poprzez NuGetta (niestety u mnie one nie działały :D), jak również możemy je ściągnąć z oficjalnej strony projektu http://caliburnmicro.codeplex.com/releases/view/81466. Następnie stwórzmy główne okno aplikacji(wspomnianego wcześniej Shella) oraz jego viewmodel. W tym celu dodajemy do projektu Silverlight User Control o nazwie ShellView
ShellView
oraz nową klasę ShellViewModel.
ShellViewModel
Dorzućmy jeszcze jakiegoś TextBox-a do naszego ShellView, tak aby mieć pewność, że rzeczywiście odpowiednie okno jest widoczne przy starcie aplikacji

W kolejnym kroku musimy stworzyć właściwy bootstrapper – dodajmy nową klasę o nazwie SilverlightBootstrapper, która dziedziczyć będzie po klasie Bootstrapper

Klasa Bootstrapper jest to klasa dostarczona przez CaliburnMicro, generyczny parametr T określa nam viewmodel na podstawie którego framework będzie wyszukiwał odpowiedni widok shella ze swojego wbudowanego kontenera IOC (Caliburn podczas uruchomienia aplikacji rejestruje dostępne typu w kontenerze). Zgodnie z domyślną konwencją, CaliburnMicro jako główne okno aplikacji ustawi widok, który nazywać się będzie ShellView. W celu odpalenia naszego bootstrappera musimy jeszcze umieścić go w zasobach aplikacji. Zmodyfikujmy zatem plik App.xaml aby wyglądał w następujący sposób

Kompilując, a następnie uruchamiając nasz projekt naszym oczom powinien ukazać się następujący widok
Client.Silverlight

Powyżej przedstawiłem w jaki sposób stworzyć najprostszą wersję bootstrapera z wykorzystaniem CaliburnMicro oraz jego domyślnego kontenera IOC. Jednakże najczęściej jest tak, że w aplikacji wykorzystujemy już jakiś bardziej zaawansowany kontener i po prostu nie chcemy wykorzystywać jednego kontenera do “rozwiązywania” widoków, a drugiego do pozostałych rzeczy. Dlatego też pokaże teraz w jaki sposób skonfigurować napisany wcześniej bootstrapper tak aby widoki były wyciągane z naszego kontenera IOC. Zacznijmy do dodania do naszego projektu referencji do NInjecta, a następnie utwórzmy klasę IOCContainer (będącą naszym customowym kontenerem), wyglądającą w następujący sposób

Następnie w klasie SilverlightBootstraper musimy przeciążyć funkcję Configure,GetAllInstances oraz GetInstance. Jako, że będziemy korzystali z naszego własnego kontenera w funkcji Configure rejestrujemy wszystkie potrzebne nam viewmodele oraz widoki. Zatem funkcja Configure powinna od teraz wyglądać tak

Następnie musimy “pokazać” Caliburn-owi gdzie powinien szukać widoków. Dlatego też przeciążamy funkcje GetInstance oraz GetAllInstances i zmieniamy ich postać na następującą

Od tej pory, za każdym razem gdy użyjemy mechanizmów Caliburna do bindowania viewmodelu z widokiem itp.,Caliburm będzie szukał widoków oraz viewmodeli w naszym kontenerze IOC.

2.2 Własna konwencja wyszukiwania widoków

W przedstawionym powyżej przykładzie bootstrappera, nasz bootstrapper korzystał z domyślnej konwencji rozwiązywania widoków na podstawie viewmodeli (do ShellViewModel został dopasowany widok ShellView). Czasem jednak konwencja ta, nie pasuje do konwencji przyjętej w danym projekcie. Sam biorę udział w projekcie, w którym widoki w kontenerze są rejestrowane następujący sposób

Czy zatem oznacza to, że nie mogę korzystać już mechanizmów Caliburna i musze zrezygnować chociażby z bootstrappera ?Oczywiście,że nie. Framework dostarcza nam możliwość zdefiniowania własnej konwencji wyszukiwania widoków na podstawie viewmodeli. W celu zastąpienia domyślnej konwencji musimy podpiąć się pod propercję LocateForModelType znajdującą się w klasie ViewLocator i zdefiniować własną funkcję wyszukującą widok na podstawie viewmodelu. W moim przypadku wygląda to w następujący sposób

Funkcja ViewLocator.LocateForModelType odpalana jest za każdym razem, gdy CaliburnMicro chce wyszukać widok na podstawie viewmodleu. Najważniejszym argumentem funkcji LocateForModelType jest modelType – czyli typ viewmodelu, który posłuży nam do znalezienia widoku – a właściwie jego typu. Po odnalezieniu typu widoku, wywołujemy funkcję

(które z kolei znajdzie nam w kontenerze pożądanym przez nas typ widoku) i zwracamy jej rezultat.

3. Bootstrapper – Windows Phone

W przypadku bootstrappera dla Windows Phona sytuacja wygląda odrobinę inaczej. W celu utworzenia własnego bootstrappera musimy rozszerzyć klasę PhoneBootstrapper. Utwórzmy zatem nowy projekt typu Windows Phone Application, oraz dodajmy do niego nową kontrolkę typu Windows Phone Portrait Page.
ShellViewPortrait
Podobnie jak w przypadku projektu Silverlightowego dodajemy referencję do CaliburnMicro ,dorzucamy klasę viewmodelu (którą nazywamy ShellViewModel) oraz dodajemy jakiegoś textboxa do ShellView. Następnie musimy utworzyć nasz bootstrapper, dodajmy zatem klasę WindowsPhoneBootstrapper rozszerzającą klasę PhoneBootstrapper. Podobnie jak w przypadku bootstrappera silverlightowego przeciążamy funkcje odpowiedzialne za konfigurację kontenera IOC.

Zwróćmy uwagę, że w kontenerze IOC zostały zarejestrowane usługi nawigacji Windows Phona, do których został przekazany RootFrame. RootFrame jest to główne okno aplikacji, zostaje ono utworzone przez CliburnMicro. Tak samo jak w przypadku projektu Silverlightowego w pliku App.xaml.cs zostawiamy jedynie konstruktor z wywołaniem funkcji InitializeComponents(). W celu ustawienia ShellView jako RootFram-a musimy zmodyfikować plik WMAppManifest.xml (znajdujący się w katalogu Properties). Odnajdujemy tam wpis

i zamieniamy go na

Jak widać WindowsPhone wymusza tutaj podejście ViewFirst. Przy uruchomieniu aplikacji mechanizm nawigacji WindowsPhona odpali widok ShellView (bez wiedzy Caliburn’a), natomiast CaliburnMicro dopasuje odpowiedni ViewModel do naszego widoku – na podstawie domyślnej konwencji. Jeżeli chcielibyśmy zmodyfikować sposób wyszukiwania ViewModeli do widoków musimy podpiąć się do propertisa LocateForViewType znajdującego się w klasie ViewModelLocator

W ostatnim kroku musimy jeszcze tylko dodać nasz bootstrapper do zasobów aplikacji

odpalając teraz program naszym oczom powinien ukazać się taki oto widok
CaliburnMicro

Tworzenie bootstrappera aplikacji przy użyciu CaliburnMicro

Lokalizowanie aplikacji WPF oraz Silverlight 5 przy użyciu MarkupExtension

W poprzednim wpisie przedstawiłem w jaki sposób można lokalizować aplikację napisaną w Silverlight 4 oraz Windows Phone, wykorzystując do tego ten sam mechanizm. Tym razem zademonstruje w jaki sposób można nieco uprościć składnie tłumaczenia wykorzystując do tego MarkupExtension.
Jeżeli kiedykolwiek pisałeś coś w Silverlighcie, Windows Phonie lub WPF-ie istnieje duża szansa, że używałeś już MarkupExtension. Do najpopularniejszych MarkupExtensions należą takie słowa kluczowe (używane w XAML-u) jak:

  • Binding
  • StaticResource
  • DynamicResource
  • TemplateBinding

Na potrzeby mechanizmu lokalizowania aplikacji WPF (ewentualnie Silverlight 5) stworzymy customowe MarkupExtenssion, które będzie odpowiedzialne za tłumaczenie elementów UI naszej aplikacji.
Zacznijmy od przygotowania plików zasobów. Podobnie jak w poprzednim wpisie utwórzmy trzy pliki:

  • Localization.resx
  • Localization.pl-PL.resx
  • Localization.en-US.resx

W plikach tych będziemy przechowywać nasze tłumaczenia. Następnie stwórzmy klasę Translator, która będzie dziedziczyła po klasie MarkupExtension.Do klasy tej dodajmy właściwość

,która będzie przechowywać klucz dzięki któremu z pliku zasobów wyciągniemy tekst w odpowiednim języku. W kolejnym kroku musimy przeciążyć funkcję

tak aby dostarczyła nam ona przetłumaczony tekst.W moim pierwszym podejściu funkcja ta wyglądała w następujący sposób

Wadami takiego rozwiązania było:

  • duża liczba tworzonych obiektów – przy każdym tłumaczeniu tworzyłem nowy obiekt Localization(), który prawdopodobnie może być dość ciężkim obiektem (zwłaszcza gdy będzie przechowywał dużo tłumaczonego tekstu)
  • brak możliwości dynamicznej zmiany języka

Ostatecznie zatem zrezygnowałem z przedstawionej wyżej opcji i zdecydowałem się na metodę odrobinę bardziej zaawansowaną. Po pierwsze utworzyłem klasę TranslationManager

która będzie zarządzała tłumaczeniami. Najważniejszą metodą tej klasy jest oczywiście funkcja

która to zwraca tekst w odpowiednim języku – w zależności od kultury, która zostanie ustawiona w TranslationManagerze. Ponadto TranslationManager posiada jedno zdarzenie

które ma za zadanie poinformować UI o potrzebie odświeżenia zbindowanych elementów. W jaki sposób się to odbywa ? Wszystko zawdzięczamy interfejsowi INotifyPropertyChanged oraz bindingom. Funkcja ProvideValue została zmodyfikowana w następujący sposób

W funkcji tej tworze binding, który binduje się do właściwości Value obiektu TranslationItem. TranslationItem jest to prosty obiekt który udostępnia właściwość Value zwracającą przetłumaczony tekst. Dodatkowo obiekt ten podpina się do zdarzenia LanguageChanged z klasy TranslationManager. W przypadku gdy ktoś zmieni język aplikacji, odpalone zostanie zdarzenie NotifyPropertyChanged, które poinformuje widok o potrzebie odświeżenia odpowiednich elementów. Klasa TranslationItem wygląda zatem w następujący sposób

Mając gotowy mechanizm tłumaczący możemy wykorzystać go w następujący sposób w XAML-u

Zauważmy, że tekst do labelki jest przypisywany z wykorzystywaniem naszego customoweog MarkupExtension – WPFMarkupExtension:Translator (WPFMarkupExtension – jest to alias na namespace, w którym znajduje się nasza klasa Translator). W składni przekazujemy do właściwości Key klucz do tekstu (znajdującego się w resourcach), który chcemy tłumaczyć. W celu zmiany języka wystarczy, że ustawimy interesującą nas kulturę w klasie TranslationManager

Przykładowy widok wykorzystujący napisany translator, oraz pokazujący dynamiczną zmianę języka może wyglądać w następujący sposób (dorzucamy następujące linijki do MainWindow.xaml)

Następnie tworzymy ViewModel do naszego okna

Ostatecznie przypisujemy obiekt klasy MainPageViewModel do DataContextu MainWindow.xaml

MarkupExtension
Kod do projektu można znaleźć pod tym linkiem
http://www.4shared.com/rar/SwDXay7V/WPFMarkupExtension.html

Lokalizowanie aplikacji WPF oraz Silverlight 5 przy użyciu MarkupExtension