Prism – nawigacja na podstawie zdefiniowanych stanów  

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2011-12-30

Wprowadzenie

Prism to nie tylko czysta biblioteka, ale również poradnik na temat pisania elastycznych i modularnych aplikacji. Jednym z ważnych elementów, we wszystkich typach aplikacji, jest sposób nawigacji między kolejnymi screen’ami. PRISM proponuje dwie metody: nawigację na podstawie stanów oraz przełączanie między widokami. Przełączanie między widokami polega na tworzeniu osobnego widoku dla konkretnego screen’a. Za każdym razem, gdy chcesz przejść do kolejnego, usuwasz aktualny i tworzysz całkowicie nowy widok. Artykuł jednak dotyczy nawigacji na podstawie stanów, realizowanych za pomocą Visual State Manager (VSM). VSM jest wbudowanym komponentem w .NET od wersji 4.0. Aby móc z niego korzystać, wcześniej programiści musieli zaopatrzyć się w WPF Toolkit.

Jak to działa?

Jak sama nazwa wskazuje, wszystko polega na zdefiniowaniu poszczególnych stanów aplikacji, a następnie na przełączaniu się między nimi. Z punktu technicznego, definiowany jest wyłącznie jeden duży widok, który zawiera poszczególne stany. Za pomocą zarządzania stanami, ukrywa się poszczególne kontrolki w widoku. Jeśli masz zatem stany A,B,C, zdefiniuj jeden główny widok, który zawiera trzy kontrolki dla stanów A, B, C. Nawigacja polega na ukrywaniu poszczególnych kontrolek – aby pokazać, np. stan B, ukryj kontrolki A oraz C. Przejścia definiowane są za pomocą wspomnianego wcześniej Visual State Manager (VSM). Za pomocą VSM można zdefiniować czas przejścia oraz towarzyszący temu typ animacji. W najprostszym przypadku nastąpi natychmiastowa zmiana stanu. W bardziej zaawansowanych scenariuszach można zdefiniować animację, np. obrót poprzedniego stanu, lub częściowe zanikanie.

Scenariusze

Nawigacja na podstawie stanów jest stosunkowo prosta w implementacji, jednak aby móc rozpoznać scenariusz, w którym stany będą poprawnie pełnić swoją rolę, należy dobrze zrozumieć jej zalety i wady. Przedstawiony w artykule typ nawigacji sprawdzi się w następujących scenariuszach:

  • Wyświetlanie tego samego typu danych w różny sposób – na przykład te same dane mogą być wyświetlane za pomocą diagramów, tabel, wykresów itp. Za pomocą stanów możesz modyfikować sposób wyświetlenia danych, wciąż jednak korzystając z tego samego ViewModel.
  • Zmiana widoku na podstawie właściwości ViewModel. Jeśli ViewModel już dysponuje właściwościami, opisującymi widok (np. IsChartVisible), wtedy stworzenie jednego dużego widoku, zawierającego poszczególne stany, jest naturalnym rozwiązaniem.

Jeśli natomiast kolejne screen’y reprezentują kompletnie różne typy danych, należy w takiej sytuacji wprowadzić dodatkowe ViewModel’e – nie powinno się umieszczać logiki dotyczącej różnych typów danych w tym samym ViewModel. W takiej sytuacji, nawigacja na podstawie stanów również stanie się trudna w użyciu i znacznie lepszym rozwiązaniem będzie zaimplementowanie nawigacji, polegającej na całkowitym przełączaniu widoków. W rozwiązaniu bazującym na stanach występuje wyłącznie jeden duży widok, a co za tym idzie, ciężko jest podłączyć kilka viewmodel’i. Jeśli Twoje wymagania potrzebują kilku ViewModel’i, skorzystaj z przełączania widoków. W przeciwnym wypadku – z nawigacji na podstawie stanów.

Implementacja

Zacznij od zdefiniowana głównego widoku, który zawiera poszczególne stany:

<StackPanel x:Name="Layout">
        <StackPanel x:Name="StateAContainer" Visibility="Collapsed">
            <Label Content="State A" FontSize="40" />
            <Button Content="State B" Command="{Binding GoToStateCmd}" 
            CommandParameter="ShowStateB"/>
        </StackPanel>
        <StackPanel x:Name="StateBContainer" Visibility="Collapsed">
            <Label Content="State B" FontSize="40" />
            <Button Content="State C" Command="{Binding GoToStateCmd}" 
                    CommandParameter="ShowStateC"/>
        </StackPanel>
        <StackPanel x:Name="StateCContainer" Visibility="Collapsed">
            <Label Content="State C" FontSize="40"/>
            <Button Content="State B" Command="{Binding GoToStateCmd}" 
                    CommandParameter="ShowStateB"/>
            <Button Content="State A" Command="{Binding GoToStateCmd}" 
                    CommandParameter="ShowStateA"/>
        </StackPanel>
</StackPanel>

Powyższy widok zawiera 3 stany. Każdy z nich posiada przynajmniej jeden przycisk do pokazania następnego stanu. W XAML zdefiniuj wszystkie stany jako ukryte (Visibility = collapsed).

Kolejnym etapem jest zdefiniowanie reguł – kiedy jaki stan ma być pokazany. Wykorzystaj do tego interakcje z Expression Blend SDK:

<i:Interaction.Behaviors>
    <ei:DataStateBehavior Binding="{Binding ShowStateA}" Value="True"  
TrueState="StateA" FalseState="HiddenStateA" ></ei:DataStateBehavior>

<ei:DataStateBehavior Binding="{Binding ShowStateB}" Value="True"
TrueState="StateB" FalseState="HiddenStateB" ></ei:DataStateBehavior>

<ei:DataStateBehavior Binding="{Binding ShowStateC}" Value="True"  
TrueState="StateC" FalseState="HiddenStateC" ></ei:DataStateBehavior>
</i:Interaction.Behaviors>

DateStateBehaviour uruchamia dany stan w zależności od powiązanej właściwości (Binding). Na przykład, gdy właściwość ShowStateA jest ustawiona na true, wtedy uruchamiany jest stan o nazwie StateA. W przeciwnym wypadku, odpalany jest HiddenStateA. Oczywiście ShowStateA musi zostać zdefiniowany w ViewModel.

Reprezentacje poszczególnych stanów zostały już zdefiniowane. Następnie, za pomocą DataStateBehaviour określono zasady przejść. Ostatnią kwestią jest wykorzystanie VisualStateManager do zdefiniowania stanów oraz ewentualnych przejść między nimi. Zacznij od prostego przykładu:

<VisualStateManager.VisualStateGroups>
           <VisualStateGroup x:Name="States">
                <VisualState x:Name="StateA">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames  Storyboard.TargetProperty="(FrameworkElement.Visibility)" Storyboard.TargetName="StateAContainer">
                            <DiscreteObjectKeyFrame>
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="HiddenStateA">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Visibility)" Storyboard.TargetName="StateAContainer">
                            <DiscreteObjectKeyFrame>
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Collapsed</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>              
        </VisualStateManager.VisualStateGroups>

Powyższe stany po prostu ukrywają lub pokazują stan A. Analogicznie, oczywiście należy zaimplementować pozostałe stany (A oraz B). Jako StoryBoard można zdefiniować dowolną animację, np. zmianę kształtu panelu w czasie przejścia:

<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)" Storyboard.TargetName="Control">
<EasingDoubleKeyFrame KeyTime="0" Value="2"/>
</DoubleAnimationUsingKeyFrames>                                           
</Storyboard>

Komunikacja między ViewModel a widokiem

Komendy służą do komunikacji między widokiem a ViewModel. Użytkownik, np. włączając przycisk, wywołuje komendę, której obsługa znajduje się w VM. PRISM dostarcza analogiczny mechanizm, działający w drugą stronę. ViewModel może wysłać pewien sygnał do widoku, aby on zmienił swój stan. Weź pod uwagę typowo akademicki przypadek wyświetlenia wiadomości z jakimś tekstem. Załóżmy, że nie chcesz wywoływać funkcji MessageBox.Show z poziomu ViewModel, a raczej przekierować tę odpowiedzialność do widoku. Prism dostarcza nowy typ InteractionRequest:

private InteractionRequest<Notification> _showMessageBox;
public InteractionRequest<Notification> ShowMessageBox
{
get { return _showMessageBox ?? (_showMessageBox = new InteractionRequest<Notification>()); }
}
Aby „wysłać sygnał” i zakomunikować tym samym zmianę stanu, wywołaj metodę Raise, przekazując opcjonalnie Title lub Content:
ShowMessageBox.Raise(new Notification(){Title = "Hello World"});
Kolejnym krokiem jest obsłużenie tego w XAML:
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding ShowMessageBox}">
                <StatedBasedNavigation:ShowMessageAction/>
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>

InteractionRequestTrigger to wyzwalacz, dostarczony przez PRISM, uruchamiany w momencie wywołania metody Raise na obiekcie InteractionRequest. Wewnątrz wyzwalacza możesz uruchomić dowolny kod. W powyższym przypadku został wykorzystany ShowMessageAction:

public class ShowMessageAction : TriggerAction<FrameworkElement>
{   
        protected override void Invoke(object parameter)
        {
            InteractionRequestedEventArgs e =
                parameter as InteractionRequestedEventArgs;
            if (e != null)
            {
                MessageBox.Show(e.Context.Title);
            }
        }
 }

Niestety, aktualnie PRISM nie zawiera żadnych implementacji akcji dla WPF. W przypadku Silverlight do dyspozycji jest np. wyświetlenie okna pop-up. Jak widać jednak, własna implementacja akcji nie jest trudna i sprowadza się do przeładowania metody Invoke.

Zakończenie

Nawigacja, przedstawiona w artykule, w zasadzie wykorzystuje wyłącznie funkcjonalność dostarczoną przez WPF oraz Expression Blend SDK. Prism odgrywa rolę przewodnika dobrych praktyk. Programiści mają do dyspozycji nawigację opartą na podstawie stanów oraz przełączaniu widoków. Zaprezentowany typ nawigacji jest prosty w implementacji, ale ogranicza się tylko do problemów opartych o jeden ViewModel. Jeśli rozwiązywany problem jest zbyt zróżnicowany i wykorzystanie jednego ViewModel’a złamałoby zasadę pojedynczej odpowiedzialności, naturalną alternatywą jest stworzenie osobnych widoków oraz viewmodel’i, a następnie przełączanie między nimi.

 


          

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.