Prism – nawigacja na podstawie zmiany widoków

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2012-02-08

Wprowadzenie

Prism dostarcza dwa mechanizmy nawigacji w aplikacjach WPF: na podstawie stanów oraz za pomocą zmiany widoków. Pierwsze podejście jest bardzo proste, ponieważ sprowadza się do utworzenia jednego widoku oraz ukrywanie jego części w zależności od danego stanu. Rozwiązanie z pewnością przyciąga zwolenników ze względu na prostotę w implementacji. Aczkolwiek w przypadku złożonej logiki podejście całkowicie jest niepraktyczne – zaawansowana logika wymaga zdefiniowania kilku ViewModel, a to jest już sprzeczne z nawigacją na podstawie stanów. Zwykle programista tworzy jeden ViewModel, eksponujący kilka właściwości opisujących aktualny stan, a następnie w widoku odpowiednie części kontrolki powiązane są z właściwościami ViewModel. Jeśli rozważany scenariusz wymaga użycia kilka ViewModel, należy zastosować inny typ nawigacji – zdefiniowanie widoku dla każdego screen’a. W momencie przejścia z jednego ekranu do drugiego, wstrzykiwany jest całkowicie nowy widok oraz ViewModel. Umożliwia to całkowitą separację poszczególnych odpowiedzialności i nie zaciemnia to niepotrzebnie struktury widoku.

Przykład – przełączenie między widokiem A i B

Dokumentacja Prism oczywiście zawiera szczegółowy opis klas oraz przykładową implementację. Moim zdaniem jednak, nawet tzw. Quickstart jest niepotrzebnie skomplikowany. W artykule przedstawiono najprostszy sposób implementacji – zwykłe przejście między widokiem A oraz B.

Jak w każdej aplikacji Prism, należy rozpocząć od implementacji bootstrapper:

public class Bootstrapper : UnityBootstrapper 
{
        protected override DependencyObject CreateShell()
        {
            return Container.TryResolve<MainWindow>();
        }
        protected override void InitializeShell()
        {         
            Application.Current.MainWindow = (Window)this.Shell;
            Application.Current.MainWindow.Show();
        }
  }

W artykule wykorzystano Unity Container, jednak w sposób analogiczny z łatwością można użyć MEF (Prism wspiera MEF, dostarczając m.in. klasę MefBootstrapper).

W tej chwili bootstrapper nic nie zawiera, z wyjątkiem inicjalizacji Shell. W prezentowanej aplikacji, shell stanowi okno do wyświetlania poszczególnych ekranów podczas nawigacji:

<ContentControl prism:RegionManager.RegionName="MainContentRegion" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>

ContentControl to standardowa kontrolka WPF umożliwiająca wyświetlenie dowolnej zawartości – w tym przypadku będą to poszczególne screen’y. Nawigacja w Prism opiera się na regionach, zatem należy ContentControl skojarzyć z jakimś regionem, przypisując właściwości (attachedproperty) RegionName dowolną wartość w postaci string’a.

Kolejnym etapem jest zdefiniowanie już samych widoków (screen’ów). Screen to nic innego jak własna kontrolka UserControl, np:

<UserControl x:Class="WpfApplication1.View1"
             xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="https://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid Background="Black" Width="300" Height="300">
        <Button Content="View 2" Click="Button1_OnClick" Height="23" HorizontalAlignment="Left" Margin="107,102,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
    </Grid>
</UserControl>

Powyższy screen zawiera przycisk, za pomocą którego użytkownik będzie mógł przejść do drugiego ekranu. W Code-Behind należy zatem umieścić stosowną implementację:

private void Button1_OnClick(object sender, RoutedEventArgs e)
   {
    RegionManager.RequestNavigate("MainContentRegion","View2");
   }

RequestNavigate jest metodą umożliwiającą nawigację do innego widoku. Pierwszy parametr to wspomniana nazwa regionu, a drugi to nazwa widoku, do którego należy przejść. Oczywiście w prawdziwej aplikacji należałoby stworzyć osobny ViewModel, ponieważ implementacja zdarzenia ButtonClick w Code-Behind jest bardzo nieelegancką praktyką. Referencję do RegionManager można uzyskać za pomocą wstrzyknięcia zależności przez konstruktor:

private readonly IRegionManager RegionManager;

public View1(IRegionManager regionManager)
{
RegionManager = regionManager;
InitializeComponent();
}

W shell należy również załadować domyślny widok, który pojawi się zaraz po starcie aplikacji. Dobrym miejscem jest np. zdarzenie Loaded:

void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_regionManager.RequestNavigate("MainContentRegion", "View1");
}

Dobrym zwyczajem jest wyodrębnienie wszelkich stałych (np. MainContentRegion) do osobnej klasy. Dzięki temu można uniknąć potencjalnych błędów (np. literówek) w przyszłych odwołaniach.

Pozostała, ostatnia kwestia do wyjaśnienia – skąd Prism ma wiedzieć, co to jest View1? Wykorzystywany jest do tego kontener i do niego należy wstrzyknąć wszelkie implementacje widoków np.:

protected override void InitializeShell()
        {
            UnityContainer container = (UnityContainer)Container;
            container.RegisterType<Object, View1>("View1");
            container.RegisterType<Object, View2>("View2");
         // Reszta inicjalizacji
        }

Należy jednak zaznaczyć, że w prawdziwej aplikacji nie powinno się tego robić w InitializeShell. Jest to odpowiedzialność poszczególnych modułów, a nie bootstrapper.

Większa kontrola nad nawigacją – interfejsy IActiveAware oraz INavigationAware

Jeśli zwykła nawigacja nie jest wystarczająca, Prism dostarcza kilka interfejsów, które mogą uczynić widoki bardziej świadome interakcji. Pierwszym z interfejsów jest IActiveAware, przeznaczony dla widoków, które potrzebują wiedzieć czy aktualnie są w stanie aktywnym. Deklaracja IActiveAware wygląda następująco:

public interface IActiveAware
{
    /// <summary>
    /// Gets or sets a value indicating whether the object is active.
    /// </summary>
    /// <value><see langword="true" /> if the object is active; otherwise <see langword="false" />.</value>
    bool IsActive { get; set; }

    /// <summary>
    /// Notifies that the value for <see cref="IsActive"/> property has changed.
    /// </summary>
    event EventHandler IsActiveChanged;
}

Do dyspozycji jest prosta flaga oraz zdarzenie wywoływane, gdy ta flaga zostanie zmieniona. Przykładowa implementacja IActiveAware przez widok:

private bool _isActive;
public bool IsActive
{
    get { return _isActive; }
    set
    {
        _isActive = value;
        if (IsActiveChanged != null)
        IsActiveChanged(this, EventArgs.Empty);
    }
}
public event EventHandler IsActiveChanged;

Następnie widok może podpiąć się do zdarzenia IsActiveChanged i odpowiednio reagować:

public View2(RegionManager regionManager)
{
    RegionManager = regionManager;
    IsActiveChanged += new EventHandler(View2_IsActiveChanged);
    InitializeComponent();
}
void View2_IsActiveChanged(object sender, EventArgs e)
{
    if (IsActive)
        MessageBox.Show("Widok jest aktualnie wyświetlany.");
    else
        MessageBox.Show("Widok nie jest aktualnie wyświetlany.");
}

Po co widok albo viewmodel mają być świadome swojej aktywności? Na przykład, aby zwolnić niepotrzebnie zajmowane zasoby, odświeżyć widok czy odpowiednio zareagować jak użytkownik chce upuścić widok a nie zapisał zmian.

Czasami istnieje potrzeba bardziej zaawansowanego mechanizmu kontroli nad nawigacją. W końcu łatwo wyobrazić sobie, że do danego widoku prowadzi kilka różnych ścieżek, np. ViewA->ViewB oraz ViewC->ViewB. Ponadto tak samo jak w przeglądarce internetowej, wpisując adres strony, w Prism również można podawać oprócz widoku, dodatkowe parametry. Ponadto Prism wprowadza mechanizm historii (journal) umożliwiający powrót do poprzednich widoków – zupełnie jak w przeglądarce internetowej (wstecz, do przodu). To wszystko jest możliwe dzięki INavigationAware, którego definicja wygląda następująco:

public interface INavigationAware
{
    bool IsNavigationTarget(NavigationContext navigationContext);
    void OnNavigatedFrom(NavigationContext navigationContext);
    void OnNavigatedTo(NavigationContext navigationContext);
}

OnNavigatedFrom oraz OnNavigatedTo stanowią parę metod wywoływanych, gdy widok jest deaktywowany (przełączany do innego) lub aktywowany. Ponadto w metodach do dyspozycji jest kontekst zawierający m.in. adres URI wraz z parametrami. Przykład:

public void OnNavigatedTo(NavigationContext navigationContext)
{
    MessageBox.Show(string.Format("Widok aktywowany: {0}", navigationContext.Uri.ToString()));
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
    MessageBox.Show(string.Format("Widok deaktyowany: {0}", navigationContext.Uri.ToString()));
}

Metoda OnNavigatedTo wywoływana jest, gdy widok jest aktywowany, a kontekst zawiera adres wywoływanego widoku wraz z parametrami. Jeśli jest to np. aplikacja bazodanowa, parametr może zawierać ID wiersza, który powinien zostać wyświetlony. Przekazanie parametru wygląda następująco:

RegionManager.RequestNavigate("MainContentRegion",new Uri("Clients?Id=4", UriKind.Relative));

Analogicznie wygląda kwestia OnNavigatedFrom – metoda wywoływana jest w momencie opuszczania widoku, a URI zawiera adres widoku, który zaraz się pojawi.

Oprócz adresu URI, parametrów wywołania, kontekst zawiera również referencję do usługi IRegionNavigationService, która umożliwia np. wykorzystanie wspomnianego dziennika (historii). Zwykle referencja pobierana jest w metodzie OnNavigatedTo, a następnie wykorzystywana według potrzeb:

private IRegionNavigationService _regionNavigationService;
public void OnNavigatedTo(NavigationContext navigationContext)
{
_regionNavigationService = navigationContext.NavigationService;            
}

Dysponując powyższą usługą, można wykorzystać przeróżne metody dziennika, np.:

if(_regionNavigationService.Journal.CanGoBack)
_regionNavigationService.Journal.GoBack();
else if(_regionNavigationService.Journal.CanGoForward)
_regionNavigationService.Journal.GoForward();

Usługa: NavigationService dostarcza ponadto kilka przydatnych zdarzeń:

public interface IRegionNavigationService : INavigateAsync
{
    IRegion Region { get; set; }
    IRegionNavigationJournal Journal { get; }
    event EventHandler<RegionNavigationEventArgs> Navigating;
    event EventHandler<RegionNavigationEventArgs> Navigated;
    event EventHandler<RegionNavigationFailedEventArgs> NavigationFailed;
}

Interfejs INaviagateAware dysponuje jeszcze metodą IsNavigationTarget. Prism, wykonując nawigację do danego widoku, przechowuje jego instancję w pamięci. Za drugim razem, gdy użytkownik chce przejeść do tego samego widoku wywołana jest metoda IsNavigationTarget. Jeśli zwróci ona true, wtedy wykorzystywana jest ponownie taka sama instancja z kontenera – w przeciwnym razie, tworzony jest nowy obiekt. Wszystko zależy od konkretnego scenariusza i architektury, jednak dzięki INavigationTarget, programista ma kontrolę nad tym.

Potwierdzenie opuszczenia widoku –IConfirmNavigationRequest

W niektórych przypadkach nie powinno się umożliwić użytkownikowi opuszczenia widoku lub przynajmniej należałoby wyświetlić odpowiednią wiadomość, która wyjaśni, co może się zdarzyć po przełączeniu do innego ekranu. Klasycznym przykładem jest zapis danych – jeśli użytkownik chce opuścić widok bez zapisu, powinno zostać wyświetlone zapytanie. Do tego właśnie służy interfejs IConfirmNavigationRequest oraz jego jedyna metoda ConfirmNavigationRequest:

public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
        if(MessageBox.Show("Czy napewno chcesz opuścić widok?",null,MessageBoxButton.YesNo)==MessageBoxResult.Yes)          
              continuationCallback(true);
        else
              continuationCallback(false);
}

Zakończenie

Mechanizm nawigacji, dostarczony przez Prism, jest dość wszechstronnym rozwiązaniem, którego własna implementacja i testowanie z pewnością zajęłoby trochę czasu. Jeśli rozważany scenariusz dotyczy głównie jednego obiektu i do jego implementacji jeden viewmodel jest wystarczający, wtedy przedstawiony w artykule sposób nawigacji może okazać się zbyt skomplikowany i lepiej użyć prostej nawigacji na podstawie stanów. Rozsądnym rozwiązaniem jest również wykorzystanie dwóch sposobów nawigacji. Nawigacja na podstawie zmiany widoków może posłużyć jako podstawowy mechanizm (dostarcza historię oraz umożliwia separację odpowiedzialności). Jeśli w obrębie konkretnych widoków, warstwa prezentacji jest zbyt skomplikowana i przedstawienie jej na jednym ekranie utrudni czytelność, wtedy dobrym pomysłem jest zaimplementowanie takiego ekranu z wykorzystaniem zmiany stanów.

 


          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.