Jak napisać aplikację wykorzystującą biblioteki PRISM 5.0 w WPF cz. 1

Ostatnimi czasy moje ścieżki programowania zaprowadziły mnie w kierunku biblioteki PRISM. W związku z czym postanowiłem podzielić się i przy okazji utrwalić swoją wiedzą w tym temacie. Ten wpis jest pierwszym z serii z tej tematyki. W tej części serii postaram się przybliżyć Wam czym jest PRISM jakie są jego główne elementy, oraz napisać prostą aplikację. W następnych odcinkach opiszę kolejne aspekty takie jak nawigacja oraz komunikacja.

1. Czym jest PRISM?
Jest to zestaw bibliotek używanych do pisania rozległych i skomplikowanych programów z zachowaniem modularnego podejścia. Jako moduł możemy rozumieć osobne programy działające w pewnym systemie tutaj zwanym Shellem. Moduły te mogą być rozwijane przez niezależne zespoły programistów, a następnie spajane w całość (rejestrowane) do shella, który jest jakby kręgosłupem aplikacji w której działają niezależne od siebie moduły. Moduły mogą się ze sobą komunikować i to zadanie ułatwiają tzw. kontenery którym może być np Unity lub MEF. Dzięki kontenerom możliwe jest wstrzykiwanie zależności (dependency injection). Pozwala to na stworzenie luźno powiązanego systemu w którym każdy komponent może być traktowany jako plugin. Każdy obiekt może mieć wstrzyknięty konstruktor, metodę lub właściwość. Dzięki takiemu podejściu duże programy/systemy piszę się o wiele wygodniej gdyż nie zależność poszczególnych podprogramów systemu znacznie ułatwia ich rozwój, utrzymanie i testowalność.

Wszystkie aplikacje pisane w wykorzystaniem tej biblioteki (a właściwie kilku bibliotek DLL) składają z kilku podstawowych elementów:

  • Shell – główny element programu, to on jest odpowiedzialny za wyświetlanie widoków z poszczególnie załadowanych modułów, w nim konfiguruje się w XAML-u układ regionów.
  • Bootstrapper – klasa inicjalizująca całą aplikację w niej konfiguruje się katalog modułów (sposób w jaki moduły maja być ładowane) jak i same moduły i niestandardowe region adaptery, ponadto rejestruję się tu kontenery, na etapie pisania klasy bootstrapera  trzeba własnie określić rodzaj kontenera. W tej przykładowej aplikacje użyje kontenera Unity.
  • Moduły – osobny podprogram uruchamiany wraz z bootstraperem lub na żądanie, To w nich będziemy programować widoki oraz view modele. Jak pisałem wcześniej moduły są od siebie nie zależne i pomiędzy modułami nie powinno być żadnych referencji.
  • Widoki i View Modele – aplikacje pisane z użyciem bibliotek PRISMa korzystają z wzorca projektowego MVVM. Każdy kto zaczął zabawę z PRISM powinien mieć opanowany ten wzorzec (zapraszam do poprzednich wpisów o tym wzorcu).
  • Regiony – Miejsca w widoku shella w którym moduły będą wyświetlać swoje widoki.
  • Nawigacja – PRISM udostępnia dwa sposoby nawigacji pomiędzy widokami modułów. Pierwszy – polega na tworzeniu nowego widoku i zastąpienie nim aktualnie wyświetlanego.  Z kolei drugi sposób polega na zmianie stanu pojedynczego widoku.
  • Event Agregator i Usługi (Services) – Aby nie tworzyć sztywnych referencji pomiędzy modułami PRISM wprowadza możliwość komunikacji pomiędzy nimi. Event Agregator polega na mechanizmie publikacji i subskrypcji, Jeden moduł publikuje informacje, a ten który był subskrybentem otrzymuję określoną informację. Usługi z kolei najczęściej wykorzystywane są do komunikacji która nie jest związana z UI np obsługa logów w systemie.

Bibliotekę i dokumentację oraz wszystkie wymagane pliki można pobrać z http://compositewpf.codeplex.com.   Pod tym adresem znajduje obszerna dokumentacja wraz z przykładowymi programami napisanymi z wykorzystaniem tej biblioteki.

Przejdźmy do kodowania. Podczas tworzenia aplikacji PRISM przejedziemy przez kilka ważnych podpunktów:

1. Stworzymć solucje z projektem Shell-a: Tworzymy nową solucje i dodajemy do niej nowy projekt. Jako typ nowego projektu dodajemy WPF Aplication project.

2. Konfigurujemy naszego Shella: Ustawiamy nasze główne okno programu.

3. Piszemy klasę inicjalizującą naszą aplikację czyli Bootstrapera: Dodajemy klasę do projektu shella, oraz konfigurujemy projekt tak aby na starcie był własnie uruchamiany bootstrapper.

4. Dodajemy pierwszy moduł: Dodajemy nowy projekt do solucji, i tworzymy klasę modułu.

5. Dodajemy widok.

6. Rejestrujemy nasz widok w klasie modułu.

 


 

1. Stworzymy solucje z projektem Shell-a:

1. Tworzymy nową solucje, i dodajemy do niej projekt WPF jak na poniższym zrzucie ekranu.

Prism projekt

 

 

 

 

 

 

 

2. Z pomocą Nugeta do solucji dodajemy pakiet PRISM. W przypadku gdy korzystamy z wersji frameworka .Net 4.5 (VS 2012 i wyżej) pobieramy wersję 5.0, w przypadku starszych wersji frameworka (VS 2010) wówczas wersję 4.0

2. Konfigurujemy naszego Shella:

1. Usuwamy MainWindow.xaml i dodajemy nowe okno WPF o nazwie ShellWindow.xaml

2. W XAML-u okna dodajemy definicje przestrzeni nazw

xmlns:prism="<a href="http://www.codeplex.com/prism">http://www.codeplex.com/prism</a>"

3. Usuwamy standardowy znacznik Grid, i w jego miejsce dodajemy:

<ItemsControl Name="MainRegion" prism:RegionManager.RegionName="MainRegion" />

Ten kod oznacza że w przypadku kiedy zarejestrujemy widok w naszym module do regionu o nazwie MainRegion, to własnie pokaże się on w tym miejscu. Shell może zawierać dowolną liczbę regionów, ustawionych w dowolny sposób. Jednakże nie do każdego rodzaju kontrolki można wstawić region. Prism podstawowo dostarcza 4 rodzaje tzw. region adapterów do których można przypisać region. Jednakże istnieje możliwość tworzenia własnych przez co możliwości tworzenia UI są nieograniczone.

Pełen kod xaml naszego okna ShellWindow:

<Window x:Class="Shell.ShellWindow"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:prism="http://www.codeplex.com/prism"
 Title="Shell" Height="300" Width="300">
 <ItemsControl Name="MainRegion" prism:RegionManager.RegionName="MainRegion" />
</Window>

3. Piszemy klasę inicjalizującą naszą aplikację czyli Bootstrapera:

1. Dodajemy nową klasę o nazwie Bootstrapper.cs , którą to klasa musisz dziedziczyć po wybranym przez nasz wcześniej kontenerze. W naszym wypadku po UnityBootstrapper. W przypadku gdy piszemy tak jak tutaj w wersji PRISM 5.0 musimy również dodać pakiet Nugeta Prism.UnityExtensions aby rozwiązać zależności.

2. Następnie musimy przeciążyć metody odpowiedzialne za inicjalizacje i stworzenie naszego okna Shell-a

protected override DependencyObject CreateShell()
{
return new ShellWindow();
}

protected override void InitializeShell()
{
base.InitializeShell();

App.Current.MainWindow = (Window)this.Shell;
App.Current.MainWindow.Show();
}

3. Ostatnia metoda jaką musimy dodać to przeciążona metoda konfiguracji katalogu modułów, jako że na tym etapie nie dodajemy żadnego modułu to póki co zostanie ona pusta.

protected override void ConfigureModuleCatalog()
 {
 base.ConfigureModuleCatalog();
 }

4. Ostatnią rzeczą jaką musimy zrobić, to zapewnić aby podczas uruchamiania naszej aplikacji pierwsza uruchamiała się klasa Bootstrapera

a) Otwieramy plik App.xaml.cs i przeciążamy metodę OnStartup.

protected override void OnStartup(StartupEventArgs e)
 {
 base.OnStartup(e);
 Bootstrapper bootstrapper = new Bootstrapper();
 bootstrapper.Run();
 }

b) Otwieramy plik App.xaml i usuwamy znacznik StartupUri=”MainWindow.xaml”

Po tych zabiega powinniśmy mieć gotowy bootstraper.

4. Dodajemy pierwszy moduł:

1. Dodajemy nowy projekt do solucji, nazwijmy go ModuleA, jak typ projektu wybieramy WPF User Control Libary. Po jego utworzeniu usuwamy domyślnie stworzoną kontrolkę UserControl1.xaml

2. Dodajemy referencje Prisma oraz Prism.UnityExtensions poprzez konfiguracje pakietu Nugeta do nowo utworzonego projektu modułu.

manageNugetPrism

 

 

 

 

 

3. W projekcie modułu dodajemy nową klasą ModuleA.cs, robimy ja publiczną oraz implementujemy interfejs IModule. Jest to główna klasa naszego modułu w której to będziemy rejestrować nasze widoki, view modele, usługi itd.

4. Dodajemy zmienną RegionManager typu IRegionManager, która posłuży nam do rejestrowania widoków do odpowiednich regionów w shellu.

5. Tworzymy konstruktor który przyjmuje wstrzyknięty IRegionManager, następnie w konstruktorze przypisujemy wcześniej wstrzyknięty regionmanager do naszej zmiennej.


public class ModuleA : IModule
{
protected IRegionManager RegionManager { get; private set; }

public ModuleA(IRegionManager regionManager)
{
RegionManager = regionManager;
}

public void Initialize()
{
throw new NotImplementedException();
}
}

Póki co metodę Initialize zostawiamy do czasu kiedy nie stworzymy widoku.

5. Dodajemy widok.

1. Do projektu Modułu dodajemy User Control i nazywamy ją np.: ModuleAView

2. Edytujemy XAML-a dodanej kontrolki dodając np:

<Label Content="Widok z Modułu A" />

2. Wracamy do metody Initialize w klasie ModuleA i za pomocą wstrzykniętego Region Managera rejestrujemy widok.

  public void Initialize()
 {
   RegionManager.RegisterViewWithRegion("MainRegion", typeof(ModuleAView));
 }

Tak zarejestrowany widok pokaże się automatycznie zaraz po inicjalizacji modułu. Jednakże póki co jeszcze nic nie zobaczymy ponieważ trzeba zarejestrować nasz moduł w klasie bootstrappera.

3. Wracamy do klasy bootstrappera i zmieniamy metodę ConfigureModuleCatalog

protected override void ConfigureModuleCatalog()
 {
 base.ConfigureModuleCatalog();

 ModuleCatalog moduleCatalog = (ModuleCatalog)this.ModuleCatalog;

 moduleCatalog.AddModule(typeof(ModuleA.ModuleA));
 }

Trzeba też nadmienić że istnieją jeszcze inne sposoby rejestrowania modułów, pokazana tutaj metoda nie jest najbardziej “elegancka” gdyż wymaga dodania refencji modułu do shella. Z innych metod dodawania to min: rejestrowanie modułów z fizycznego folderu na dysku, czy też z plików konfiguracyjnych.

Tak skonfigurowana prosta aplikacja PRISM uruchamiająca jeden moduł i pokazująca jeden widok jest już gotowa. W następnej części rozbudujemy trochę ten przykład o drugi moduł wraz widokiem i view modelem, oraz przełączaniem się pomiędzy tymi widokami za pomocą nawigacji.

 

Źródła projektu można pobrać z mojego GitHuba: link