Budowanie aplikacji WPF i Silverlight przy pomocy pojedynczej bazy kodu z wykorzystaniem Prism
Autor: Erwin van der Valk
Wskazania Composite Application Guidance for WPF and Silverlight, znane również pod nazwą Prism v2, są już dostępne od kilku miesięcy. Jednym z obszarów, w których Prism zapewnia wsparcie jest możliwość tworzenia swojej aplikacji zarówno dla Windows Presentation Foundation (WPF), jak i dla Silverlight. Ciekawe, że początkowo ta część naszych wskazań spotkała się ze sporym oporem. Dlaczego skupialiśmy się na wieloplatformowości przez pierwsze kilka iteracji projektu, gdy mogliśmy poświęcić swój czas na udzielanie wskazówek dotyczących struktury aplikacji? Od momentu ukazania się projektu Prism v2 zauważyliśmy, że wielu naszym klientom naprawdę podoba się ta część wskazań. Szczególnie podoba im się narzędzie Project Linker, które zbudowaliśmy jako pomoc dla wieloplatformowości. Choć jest proste, narzędzie to zostało dobrze przyjęte i jest używane w stopniu, którego nie mogliśmy sobie wyobrazić.
Przyjrzyjmy się więc dotychczasowemu podejściu do pisania aplikacji wieloplatformowych i jak Prism może nam w tym pomóc.
Wstęp
Gdy zaczynaliśmy prace nad Prism v2 w sierpniu 2008, wiedzieliśmy, że chcemy obsługiwać zarówno WPF, jak i Silverlight. Silverlight wielkimi krokami zmniejsza dystans pomiędzy możliwościami rozbudowanej aplikacji klienckiej, a zasięgiem i łatwością wdrażania aplikacji WWW. Pierwsza wersja Prism jest nakierowana tylko na WPF, ale ponieważ WPF i Silverlight są bardzo podobnymi technologiami, wiedzieliśmy, że nietrudno będzie utworzyć wersję dla Silverlight. Ponieważ Prism pomaga w pisaniu luźno powiązanych aplikacji modułowych, to sądziliśmy, że Prism może nam też pomóc w umożliwianiu programistom pisania aplikacji przeznaczonych zarówno dla WPF, jak i Silverlight przy użyciu tej samej bazy kodu. Wyzwanie stanowi oczywiście fakt, że pomimo podobieństw pomiędzy WPF i Silverlight nie są to technologie kompatybilne na poziomie binarnym. Interfejs API i sam język XAML również się nieco różnią, co utrudnia jednoczesne tworzenie kodu dla wielu platform.
Czym jest wieloplatformowość i dlaczego powinniśmy się nią zająć.
Wieloplatformowość jest zdolnością aplikacji do obsługi wielu platform z pojedynczej bazy kodu. W tym przypadku mówimy o obsłudze zarówno normalnej wersji 3.5 pulpitowej platformy Microsoft .NET Framework, która zawiera WPF oraz wersji .NET Framework dla platformy Silverlight.
Dlaczego powinniśmy się zajmować tą możliwością? Najbardziej oczywistym powodem byłoby to, że chcielibyśmy wykorzystać silne strony WPF i Silverlight. W WPF możemy budować aplikacje, które w pełni wykorzystują platformę kliencką i komunikują się z istniejącymi aplikacjami takimi jak Microsoft Office. Jest też dużo łatwiej ponownie wykorzystywać istniejące zasoby, na przykład poprzez współpracę z technologiami COM lub Windows Forms.
Podczas gdy WPF daje nam więcej możliwości, to Silverlight daje nam znacznie szerszy zasięg, ponieważ działa na wielu platformach systemowych. Działa również w chronionej piaskownicy, więc użytkownicy mogą bezpiecznie instalować Silverlight bez konieczności posiadania uprawnień administracyjnych. Nasza aplikacja niekoniecznie musi być całkiem wieloplatformowa, aby warte to było zachodu. Na przykład moglibyśmy udostępniać niewielkie części aplikacji wewnętrznych przez Internet, aby dać klientom możliwość przeglądania i zmieniania pewnych informacji.
Tworzenie wersji Prism dla Silverlight
Chociaż czuliśmy, że wieloplatformowość stanowiłaby cenną możliwość dla naszych klientów, to dodaliśmy ją też z nieco egoistycznych pobudek. Chcieliśmy zbudować wersję Prism dla Silverlight, ale nie chcieliśmy dodatkowych kosztów związanych z utrzymywaniem podwójnej bazy kodu. Ponieważ nie wiedzieliśmy, ile kodu będziemy w stanie ponownie wykorzystać, wykonaliśmy kilka „skoków”. „Skokiem” (w terminologii programowania zwinnego – agile) jest ograniczone czasowo badanie, które pozwala nam zapoznać się lepiej z dziedziną problemu, abyśmy mogli dokonać lepszego oszacowania. Wykonaliśmy więc kilka „skoków”, aby zobaczyć, jaką część bazy kodu Prism v1 moglibyśmy przenieść do Silverlight i jak trudne byłoby przeniesienie naszego kodu do Silverlight. Konkluzja była fascynująca. Oszacowaliśmy, że moglibyśmy ponownie wykorzystać około 80 procent kodu z biblioteki Prism bez modyfikowania go.
W idealnym przypadku chcielibyśmy być w stanie stworzyć pojedynczy projekt i kompilować go zarówno dla WPF, jak i dla Silverlight. Problemem przy tym podejściu jest to, że system projektu w Visual Studio zakłada, iż projekt ma jeden zestaw odwołań, jeden kompilator i jeden typ danych wyjściowych. Wypróbowaliśmy kilka sposobów na to, aby pojedynczy projekt dawał wyniki zarówno dla Silverlight, jak i dla WPF, ale żaden z nich nie działał tak, aby nas zadowolić.
Podejściem, które działało naprawdę dobrze, było tworzenie dwóch projektów i łączenie plików z jednego projektu z drugim. Świetną cechą tego podejścia jest to, że mamy dokładną kontrolę nad tym, jak powinien wyglądać wynik w każdym przypadku. Na przykład możemy decydować, które pliki powinny być wspólne albo do których podzespołów powinny istnieć odwołania w każdym projekcie. Jakakolwiek zmiana w przyłączonym pliku jest natychmiast odzwierciedlana w obu projektach.
Tworzenie narzędzia Project Linker
Po fazie badań uzgodniliśmy, że podejście z łączonymi plikami działa, chociaż jest dość uciążliwe i podatne na błędy. Prowadziliśmy wtedy mnóstwo dyskusji. Czy powinniśmy zainwestować znaczną ilość czasu w budowę narzędzia pomagającego w wieloplatformowości zamiast poświęcać ten czas na tworzenie wytycznych? Może nie jest to oczywiste, ale zbudowanie i dostarczenie narzędzia, które integruje się z Visual Studio jest dla nas dość kosztowne. Integracja z Visual Studio zajmuje trochę czasu sama z siebie. Do tego trzeba dodać czas potrzebny na zbudowanie instalatora podpisanego zgodnie ze ścisłymi wewnętrznymi wskazówkami firmy Microsoft dotyczącymi podpisywania kodu i przetestowanie narzędzia w wielu różnych środowiskach. Wszystkie te czynniki ograniczają ilość czasu, który możemy poświęcić na tworzenie faktycznych wskazówek. Ponieważ jednak byliśmy mocno przekonani, że to narzędzie pomoże nam w naszych wysiłkach i będzie bardzo przydatne dla naszych klientów, postanowiliśmy działać i je zbudować.
Przez kilka pierwszych dwutygodniowych iteracji pracowaliśmy niemal wyłącznie nad narzędziem Project Linker. Jak wspominałem wcześniej, spotkaliśmy się z reakcją społeczności. Dlaczego poświęcamy czas na wieloplatformowość? Czy Prism nie jest projektem pomagającym budować niejednorodne aplikacje? Jednakże podczas budowania narzędzia Project Linker jednocześnie korzystaliśmy z niego do tworzenia wersji Prism dla Silverlight. A ponieważ korzystaliśmy z narzędzia Project Linker (wewnętrznie nazywamy to wyjadaniem jedzenia własnemu psu), byliśmy przekonani, że nasze narzędzie będzie bardzo użyteczne.
Celowo staraliśmy się, aby Project Linker był bardzo prosty. To co robi, nie jest magiczne, a wszystko, co można zrobić przy użyciu narzędzia Project Linker, można też łatwo zrobić ręcznie. Jak wskazuje nazwa, można połączyć dwa projekty razem, jak pokazano na Rysunku 1. Dowolny plik, który dodamy do pierwszego projektu, będzie dodany jako plik połączony do drugiego projektu. Zmiany takie jak przenoszenie, zmienianie nazwy lub usuwanie również będą odzwierciedlone.
Rysunek 1: Korzystanie z narzędzia Project Linker do łączenia plików pomiędzy projektami.
Ponieważ moglibyśmy chcieć sprawować kontrolę nad tym, które pliki łączyć, zaimplementowaliśmy prostą konwencję nazewniczą. Domyślnie wszystkie pliki poza tymi z rozszerzeniem .xaml są łączone. Jeśli jednak chcemy tworzyć pliki, które są specyficzne dla jednej platformy, możemy dołączać do nich przyrostek .Desktop lub .Silverlight. Jeśli chcemy, możemy zmieniać tę konwencję nazewniczą. W pliku projektu można znaleźć wyrażenie regularne, które określa, czy plik musi być łączony. Poniżej pokazano przykład:
ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?Silverlight
(\\.*)?$;\.desktop;\.Silverlight;\.xaml;^service references(\\.*)?$;\. clientconfig;^web references(\\.*)?$"
Narzędzie Project Linker okazało się nieocenione przy tworzeniu Prism v2. Po przyzwyczajeniu się do drobnych osobliwości tego podejścia, czym zajmiemy się później, przyjemnym doświadczeniem była możliwość pisania kodu raz, a następnie kompilowania go od razu w wersji dla Silverlight i dla WPF. Po pewnym czasie członkowie projektu mieli listy funkcji dodatkowych, które według nich powinny się znaleźć w narzędziu Project Linker, ale nawet bez tych funkcji narzędzie to było bardzo przydatne. Prostota narzędzia uczyniła go znacznie bardziej uniwersalnym, niż to sobie wyobrażaliśmy. Po opublikowaniu w witrynie CodePlex słyszeliśmy od ludzi, że nie korzystają z tego narzędzia tylko do tworzenia aplikacji Silverlight i WPF. Niektórzy korzystali z niego do łączenia razem różnych projektów Visual C++. Inni korzystali z niego w celu wykorzystywania tego samego zestawu reguł sprawdzania poprawności biznesowej w aplikacji Silverlight i w usługach Web wykorzystywanych przez tę aplikację Silverlight.
Programowanie sterowane testami w Silverlight
Innym obszarem, w którym dla mnie osobiście wieloplatformowość okazała się wygodna, jest sterowane testami wytwarzanie oprogramowania w Silverlight. Jestem wielkim fanem programowania sterowanego testami. Chociaż dla Silverlight dostępna jest wspaniała platforma do testów jednostkowych, to ma pewne niedociągnięcia. Nie jest zintegrowana z Visual Studio i nie można wybrać, który test ma być uruchomiony. Wykonane zostaną wszystkie testy w podzespole testowym.
Teraz więc nawet jeśli piszę aplikację, która będzie działać tylko w Silverlight, to nadal tworzę połączoną wersję tego projektu w WPF, tylko do pisania i uruchamiania testów jednostkowych. Uwielbiam mieć możliwość napisania testu, a następnie uruchomienia go bezpośrednio z Visual Studio. W ciągu sekund mam wyniki swojego testu, więc cykle „kod -> test -> poprawka -> test” są bardzo krótkie. To naprawdę zwiększa moją efektywność. Nie należy zapominać o uruchomieniu testów jednostkowych dla Silverlight przed zatwierdzeniem kodu, ponieważ czasami test powiedzie się na jednej platformie, a nie powiedzie się na innej.
Projektowanie aplikacji pod kątem wieloplatformowości
Podczas budowania wersji biblioteki Prism dla Silverlight, referencyjnej implementacji aplikacji Stock Trader oraz przewodników QuickStart poznaliśmy kilka cennych najlepszych praktyk dotyczących wieloplatformowości. Najważniejszą z nich jest to, że luźno powiązana i modularna architektura może nam naprawdę pomóc w dążeniu do wieloplatformowości. Luźnie powiązane architektury pozwalają nam wybierać, które elementy naszej aplikacji chcielibyśmy wykorzystać na wielu platformach.
Rozpoznawanie, co można łatwo wykorzystać na wielu platformach
Zanim będziemy mogli opracować architekturę swojego rozwiązania tak, aby obsługiwało wieloplatformowość, ważne jest, aby wiedzieć, co może, a co nie może być łatwo wykorzystane na wielu platformach. Można ogólnie powiedzieć, że większość kodu związanego z logiką biznesową można bardzo łatwo wykorzystać na wielu platformach. Na przykład:
• Logika prezentacyjna. Jest to logika, która odpowiada na działania użytkownika i określa, które dane są przekazywane do elementów wizualnych.
• Logika biznesowa i reguły biznesowe. Logika biznesowa steruje procesem biznesowym, a reguły biznesowe mogą dokonywać sprawdzania poprawności jednostek biznesowych.
• Jednostki biznesowe. Są to klasy, które reprezentują dane w naszej aplikacji.
Kod, który jest bardziej związany z infrastrukturą jest zwykle bardzo trudny do wykorzystania na wielu platformach. Oto przykłady:
• Elementy wizualne (widoki). Sposób określania elementów wizualnych takich jak formanty różni się pomiędzy WPF i Silverlight na tyle, że trudno je wykorzystywać na wielu platformach. Nie dość, że różne formanty są dostępne dla każdej platformy, to język XAML używany do określania ich układu również na różne możliwości. Choć nie jest niemożliwym wykorzystanie na wielu platformach bardzo prostych widoków lub stylów, to szybko można się natknąć na ograniczenia.
• Ustawienia konfiguracyjne. Silverlight nie zawiera przestrzeni nazw System.Configuration i nie obsługuje plików konfiguracyjnych. Jeśli chcemy sprawić, aby nasza aplikacja Silverlight była konfigurowalna, to musimy zbudować rozwiązanie niestandardowe.
• Dostęp do danych. Jedynym sposobem, w jaki aplikacja Silverlight może mieć dostęp do danych jest pośrednictwo usług Web. W przeciwieństwie do WPF, aplikacja Silverlight nie może mieć bezpośredniego dostępu do baz danych.
• Interoperacyjność (współpraca z innymi aplikacjami, COM lub Windows Forms). Aplikacja WPF w środowisku pełnego zaufania może komunikować się z innymi aplikacjami na komputerze lub wykorzystywać istniejące zasoby takie jak obiekty COM albo Windows Forms. Nie jest to możliwe w technologii Silverlight, ponieważ działa ona w chronionej piaskownicy.
• Dzienniki i śledzenie. Z powodu chronionej piaskownicy aplikacja Silverlight nie może rejestrować informacji w Dzienniku zdarzeń, ani zachowywać informacji w pliku (poza izolowaną przestrzenią dyskową).
Aby zaprojektować aplikację, która pozwoli nam łatwo wykorzystywać powtórnie naszą logikę biznesową, powinniśmy starać się oddzielać elementy, które łatwo wykorzystać na wielu platformach od elementów, które trudno wykorzystać na wielu platformach. Co ciekawe, taka dokładnie jest architektura typowej aplikacji Prism. Rysunek 2 pokazuje typową architekturę aplikacji Prism.
Rysunek 2: Typowa architektura aplikacji Prism.
Na tym diagramie widoki są klasami, które zajmują się aspektem wizualizacyjnym aplikacji. Zazwyczaj są to formanty i strony, a w przypadku aplikacji WPF lub Silverlight ich układ jest często zdefiniowany w XAML. Logika naszej aplikacji jest wyodrębniona w oddzielnych klasach. Zagłębię się nieco bardziej w kryjące się za tym wzorce projektowe, gdy będę mówić o wzorcach oddzielonej prezentacji.
Usługi aplikacyjne na tym diagramie mogą zapewniać różnorodną funkcjonalność. Na przykład składnik obsługi dziennika lub dostępu do danych może być traktowany jako usługa aplikacyjna. Prism oferuje również parę takich usług, na przykład RegionManager albo XapModuleTypeLoader. Omówię te usługi dokładniej, gdy będę mówić o budowaniu usług specyficznych dla danej platformy.
Oddzielona prezentacja
Jako część wytycznych, które zapewniamy w Prism, zalecamy, aby oddzielać aspekt wizualizacyjny aplikacji od logiki prezentacyjnej. W osiągnięciu tego może pomóc wiele wzorców projektowych takich jak Model-View-ViewModel albo Model-View-Presenter. Wspólnym elementem większości tych wzorców jest to, że opisują one, jak podzielić kod (i znaczniki) związany z interfejsem użytkownika na oddzielne klasy, z których każda odpowiada za co innego. Rysunek 3 pokazuje przykład wzorca Model-View-ViewModel.
Rysunek 3: Przykład wzorca Model-View-ViewModel.
Klasa Model obsługuje kod zawierający dane i zajmujący się dostępem do danych. Widok (View) jest zwykle formantem zawierającym kod (najlepiej w formie znaczników XAML), który wizualizuje pewne dane w naszym modelu i modelu widoku (ViewModel). Jest też klasa zwana modelem widoku (ViewModel), modelem prezentacyjnym (PresentationModel) lub prezenterem (Presenter), która obsługuje możliwie dużą część logiki interfejsu użytkownika. Zwykle implementuje się wzorzec oddzielonej prezentacji, aby dało się testować jak najwięcej kodu związanego z interfejsem użytkownika. Ponieważ kod w naszych widokach jest znany z tego, że trudno poddaje się testom jednostkowym, to te wzorce oddzielonej prezentacji pomagają nam umieścić możliwie dużo kodu w klasie ViewModel, którą można testować. Najlepiej, gdybyśmy nie mieli żadnego kodu w widokach, a jedynie nieco znaczników XAML definiujących wizualne aspekty naszej aplikacji i trochę wyrażeń wiążących pozwalających wyświetlać dane z naszego modelu i modelu widoku.
Jeśli chodzi o wieloplatformowość, to wzorzec oddzielonej prezentacji ma jeszcze jedną ważną zaletę. Pozwala nam ponownie wykorzystać całą naszą logikę interfejsu użytkownika, ponieważ została ona wydzielona w osobnych klasach. Choć nie jest niemożliwe, aby część kodu z widoków wykorzystywać na wielu platformach (XAML, formanty i wbudowany kod), to odkryliśmy, że różnice pomiędzy WPF a Silverlight są na tyle duże, że wykorzystanie naszego XAML na wielu platformach nie jest praktyczne. XAML ma różne możliwości, a formanty, które są dostępne dla WPF i Silverlight nie są takie same. To nie tylko wpływa na XAML, ale również na stojący za nim kod.
Choć nie jest prawdopodobne, że będziemy mogli ponownie wykorzystać cały nasz kod związany z interfejsem użytkownika, to wzorzec oddzielonej prezentacji pomaga nam ponownie wykorzystać możliwie dużo logiki prezentacyjnej.
Budowanie usług specyficznych dla platformy
Podczas budowania bibliotek Prism oraz implementacji referencyjnej Stock Trader ściśle stosowaliśmy się do zasady pojedynczej odpowiedzialności. Ta zasada opisuje, że każda klasa powinna mieć tylko jeden powód do zmiany. Jeśli klasa obsługuje wiele spraw lub ma więcej niż jeden obowiązek, to ma wiele powodów do zmiany. Na przykład klasa, która może załadować raport z bazy danych i wydrukować ten raport, może się zmienić, jeśli baza danych się zmieni lub jeśli układ raportu się zmieni. Ciekawy wskaźnik, czy nasza klasa robi zbyt wiele: jeśli odkryjemy, że mamy trudność w określeniu nazwy dla naszej klasy, która opisywałaby jej obowiązki, to ma ona zbyt wiele obowiązków.
Jeśli będziemy stosować się do zasady pojedynczej odpowiedzialności, to częściej będziemy mieć do czynienia z wieloma mniejszymi klasami, z których każda będzie miała swoje własne, odrębne zadanie i opisową nazwę. Często uważamy wiele z tych klas za usługi aplikacyjne, ponieważ zapewniają usługi naszej aplikacji.
Ta zasada pojedynczej odpowiedzialności naprawdę pomaga, jeśli chodzi o wieloplatformowość. Weźmy na przykład proces ładowania modułu w Prism. Wiele aspektów tego procesu jest podobnych dla WPF i Silverlight. Pewne podobieństwa obejmują to, jak ModuleCatalog śledzi, które moduły są obecne w systemie i jak ModuleInitializer tworzy wystąpienia modułu oraz wywołuje dla nich metodę IModule.Initialize(). Ale znów to, jak ładujemy pliki podzespołów, które zawierają moduły, różni się dosyć pomiędzy WPF i Silverlight. Ilustruje to Rysunek 4.
Rysunek 4: Ładowanie modułu w Prism.
Jest całkiem uzasadnione, aby aplikacja WPF ładowała swoje moduły z dysku. To właśnie robi FileModuleTypeLoader. Jednakże nie ma to sensu w przypadku aplikacji Silverlight, ponieważ jej chroniona piaskownica nie daje dostępu do systemu plików. Natomiast w przypadku Silverlight będzie nam potrzebny XapModuleTypeLoader, aby ładować moduły z pliku .xap.
Ponieważ utworzyliśmy mniejsze klasy, każdą z oddzielną odpowiedzialnością, to było znacznie łatwiej ponownie wykorzystać większość tych klas i tworzyć jedynie usługi specyficzne dla platformy, aby ująć zachowanie różniące się między platformami.
Unikanie niespójności i próba utrzymywania pojedynczej bazy kodu
Chociaż większość funkcjonalności w Prism łatwo było przenieść do Silverlight, to nieuniknione było dojście do sytuacji, w których musielibyśmy polegać w WPF na funkcji, która nie istnieje w Silverlight. Jedną z nich jest dziedziczenie właściwości zależnościowej. W WPF można by było ustawić właściwość zależnościową dla formantu i byłaby ona automatycznie dziedziczona przez jego obiekty podrzędne. Korzystaliśmy z tej możliwości do kojarzenia regionu z menedżerem regionu. Niestety automatyczne dziedziczenie właściwości nie jest dostępne w Silverlight.
W przypadku Silverlight musieliśmy tworzyć rozwiązanie, które opóźniało tworzenie regionów aż do czasu, gdy menedżer regionów mógł być zlokalizowany przez jakiś inny mechanizm. Dzięki użyciu kilku sztuczek mogliśmy ponownie wykorzystać ten kod w WPF. Moglibyśmy zatrzymać oryginalne, znacznie prostsze rozwiązanie dla WPF i korzystać z nowego rozwiązania tylko w przypadku Silverlight, ale wtedy musielibyśmy utrzymywać dwie bazy kodu i oferować różne publiczne interfejsy API.
Starając się budować funkcjonalność do wykorzystania zarówno w WPF, jak i Silverlight, nie unikniemy napotkania sytuacji, gdzie jedna z platform nie obsługuje funkcji, którą chcemy wykorzystać. Naszą najlepszą obroną przed tymi sytuacjami jest próba obejścia tych „niekompatybilności” i tworzenie rozwiązania, które działałoby w obu środowiskach. Utrzymywanie pojedynczej bazy kodu jest znacznie łatwiejsze niż utrzymywanie dwóch baz kodu.
Uwzględnianie różnych możliwości platform
Istnieją przypadki, gdy obejście różnic w platformach nie ma sensu lub nie jest możliwe, na przykład, gdy nie ma wspólnego rozwiązania, które działałoby zarówno w WPF, jak i Silverlight. Gdy tak się dzieje, to mamy do rozważenia kilka strategii. Wszędzie za wyjątkiem niewielkich i odizolowanych różnic między platformami zalecałbym budowanie usług specyficznych dla platform. Jednakże w przypadku niewielkich różnic między platformami można by rozważyć albo kompilację warunkową, albo klasy częściowe.
Kompilacja warunkowa
Najprostszą rzeczą do zrobienia w celu uwzględnienia różnych możliwości platform jest wykorzystanie kompilacji warunkowej. Korzystając z instrukcji prekompilatora #if SILVERLIGHT pokazanej na Rysunku 5 możemy tworzyć sekcje kodu, które są kompilowane tylko dla Silverlight lub tylko dla WPF. To wydaje się bardzo wygodne, ale metoda lub klasa może się szybko stać nieczytelna przy tym podejściu.
Rysunek 5: Kompilacja warunkowa.
#if SILVERLIGHT
Application.Current.RootVisual = shell;
#else
shell.Show();
#endif
Odkryliśmy, że powinniśmy stosować dyrektywę #if prekompilatora tylko do sporadycznej zmiany pojedynczego wiersza kodu. Ponieważ cierpi na tym czytelność, nie zalecam korzystania z tej techniki poza bardzo prostymi przypadkami, jak ten.
Klasy częściowe
Inną techniką, którą moglibyśmy zastosować jest wykorzystanie klas częściowych. Korzystając z tej techniki możemy tworzyć klasy, które są w większości wspólne, ale różnią się jedną lub dwoma niewielkimi metodami. Ta technika może być bardzo użyteczna przy dostosowywaniu niewielkich zmian implementacyjnych pomiędzy WPF a Silverlight.
Uważam klasy częściowe za szczególnie użytecznie w obszarze wyjątków. W .NET Framework często zaleca się, aby wyjątki mogły być serializowane. Jednakże Silverlight nie obsługuje atrybutu [Serializable]. Korzystając z klas częściowych możemy ponownie wykorzystywać większość kodu wyjątku, ale możemy zastosować atrybut [Serializable] tylko w wariancie kodu dla .NET Framework, jak pokazano na Rysunku 6.
Rysunek 6: Klasy częściowe dla wyjątków.
// MyException.cs
Public partial class MyException : Exception
{
. . .
}
// Dodatki do tej klasy wykorzystywane tylko w aplikacjach pulpitowych.
// MyException.Desktop.cs
[Serializable]
Public partial class MyException : Exception
{
protected MyException (SerializationInfo info, StreamingContext context) : base(info, context) { }
}
Niestety klasy częściowe mają problem z czytelnością i dostępnością. Nie jest od razu jasne, w którym pliku znajduje się nasza funkcjonalność. Klasa powinna mieć pojedynczy obszar odpowiedzialności i nazwę odzwierciedlającą ten jeden obowiązek. Jeśli okaże się, że klasa robi coś w jeden sposób w Silverlight i w inny sposób w WPF, to nie będzie zgodna z wzorcem pojedynczej odpowiedzialności. Wyciągnięcie kodu specyficznego dla platformy do usług z opisową nazwą jest zwykle lepszym rozwiązaniem.
Ponieważ program Visual Studio nigdy nie był projektowany do obsługi wieloplatformowości, to prawdopodobnie zetkniemy się z pewnymi problemami powodowanymi przez to podejście. Żaden z nich nie jest bardzo poważny, ale dobrze wiedzieć, jakie one są.
Visual Studio wie, że połączony plik może być umieszczony w różnych typach projektów. W zależności od tego, czy otworzymy połączony plik z WPF, czy Silverlight, to odpowiednio zostanie użyta technologia IntelliSense. Często się może zdarzyć, że będziemy korzystać z konstrukcji językowej, która jest dostępna tylko w jednej z platform. Napotkamy błędy budowania przy kompilowaniu naszego rozwiązania, ponieważ nasz kod będzie nieprawidłowy dla jednej z platform. W zależności od tego, czy otwarliśmy plik z WPF, czy Silverlight, zdecyduje to o tym, czy będą pojawiać się czerwone podkreślenia. Gdy już przyzwyczaimy się do faktu, że zmiana w projekcie może zepsuć połączony z nim projekt, to szybko będziemy wiedzieć, gdzie szukać.
Odwołania w projektach również są ciekawe. Narzędzie Project Linker nie dodaje odwołań automatycznie. Zajmowaliśmy się tym podczas budowania tego narzędzia, ale było tyle przypadków granicznych, że trudno było je uwzględnić. Na przykład pliki binarne dla Silverlight mają inne nazwy, niż pliki binarne dla WPF.
Wreszcie możemy dołączać niemal dowolny typ pliku, na przykład pliki zasobów. Jednakże powinniśmy się upewnić, że właściwości takie jak opcje kompilacji, będą takie same w obu projektach.
Wielu naszym klientom bardzo podoba się Prism. Nawet jeśli program nie potrzebuje możliwości tworzenia złożonych aplikacji oferowanych przez Prism, to może w znacznym stopniu skorzystać z elastycznej architektury promowanej przez Prism. Możliwość łatwego wykorzystania naszej aplikacji na wielu platformach jest tylko jedną z tych korzyści – a jest ona bardzo miła! Plik binarny i pliki kodu źródłowego dla narzędzia Project Linker można pobrać ze stron Microsoft Download Center. Więcej na temat Prism można dowiedzieć się online.
Erwin Van der Valk jest programistą w zespole patterns and practices w firmie Microsoft. Jest również nurkiem, mistrzem kung fu i gitarzystą metalowym. Dać mu kod, a zaraz da w niego nura robiąc przy tym wiele hałasu! Hałas, jaki robi można sprawdzić pod adresem erwinvandervalk.net.