Prism - Zdarzenia i komendy  

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2011-12-23

Wprowadzenie

Komendy oraz zdarzenia stanowią podstawowy mechanizm komunikacji między komponentami w środowisku .NET. Prism rozszerza standardowe implementacje o tzw. Composite Commands oraz Event Aggregation. Umożliwiają one uruchomienie kilku metod na wskutek danej akcji.

Composite Commands oraz Delegate Commands

Composite Commands najlepiej rozważyć na praktycznym przykładzie. Wyobraź sobie, że chcesz zaimplementować funkcje zapisu w aplikacji składającej się z wielu dokumentów. Przykładem jest np. Visual Studio – mamy jeden przycisk „Zapisz wszystko” po wciśnięciu, którego zapisywane są wszystkie otwarte dokumenty. Ponadto każdy dokument może zawierać różny typ danych, a co za tym idzie, w kodzie jest on zaimplementowany przez specyficzny ViewModel. Sama funkcja zapisu także się różni – inaczej będzie zapisywany plik tekstowy, a inaczej graficzny. Zatem potrzebna jest jedna główna komenda: „Zapisz wszystko”, która będzie odpowiedzialna za komunikację z obsługą zapisu komend dla specyficznych plików.

Delegate Commands to z kolei prosta klasa, dostarczona przez Prism, implementująca podstawowy interfejs ICommand. W swoich projektach czytelnik prawdpodobnie często wykorzystywał własną implementację (inna nazwa to RelayCommand). Implementacja DelegateCommand po prostu deleguje wykonanie komendy do metody zdefiniowanej poprzez parametr Execute. Podobnie zostaje oddelegowane sprawdzanie, czy komenda może zostać wykonana (metoda CanExecute). Przykład użycia:

  1. Deklaracja komendy:
public ICommand SaveCmd { get; set; }
  1. Inicjalizacja:
SaveCmd = new DelegateCommand(Save, CanSave);
  1. Implementacja metod:
private void Save()
        {
            // logika zapisu
        }
        private bool CanSave()
        {
            // sprawdzenie, czy metoda może być wykonana
            return false;
        }
  1. Przykład wykorzystania tej metody w języku XAML:
<Button Command="{Binding SaveCmd}"/>

Gdy metoda CanSave zwróci false, przycisk automatycznie będzie zablokowany (wówczas pojawi się szary kolor). Ponadto istnieje możliwość przekazania parametru komendzie. Wtedy sygnatury metod obsługujących będą wyglądać następująco:

private void Save(object pars)
private bool CanSave(object pars)

Z kolei inicjalizacja komendy zawierać będzie typ parametru:

SaveCmd = new DelegateCommand<object>(Save, CanSave);

W języku XAML parametr przekazywany jest poprzez atrybut CommandParameter:

<Button Command="{Binding SaveCmd}" CommandParameter="parametr" Content="Test it" />

Opisany powyżej typ komendy raczej był już znany czytelnikom. Wróćmy, więc do dużo ciekawszego typu – CompositeCommand.

  1. Deklaracja wygląda analogicznie, jednak ze względu na specyficzne dla CompositeCommand funkcje rezygnujemy z deklaracji za pomocą interfejsu:
public CompositeCommand SaveAllCmd { get; set; }
  1. Jak już zostało wspomniane, CompositeCommand to komenda, która zawiera kilka zależnych od siebie komend (child commands). Rejestracja podrzędnych komend dokonywana jest za pomocą metody RegisterCommand. Innymi słowy, CompositeCommand to kolekcja komend, do których możesz odwoływać się za pomocą jednej komendy.
SaveAllCmd.RegisterCommand(SaveFileACmd);
SaveAllCmd.RegisterCommand(SaveFileBCmd);
  1. SaveFileACmd oraz SaveFileBCmd to zwykłe, wspomniane wyżej DelegateCommand (inicjalizacje należy umieścić przed CompositeCommand):
SaveFileACmd = new DelegateCommand(SaveFileA, CanSaveFileA);
SaveFileBCmd = new DelegateCommand(SaveFileB, CanSaveFileB);
  1. W języku XAML zastosuj CompositeCommand tak, jak zwykłą komendę:
<Button Command="{Binding SaveAllCmd}" Content="Test it"/>

Po naciśnięciu przycisku, wszystkie DelegateCommand (SaveFileA, SaveFileB) zostaną uruchomione. Analogicznie, w przypadku, gdy przynajmniej jedna z zależnych komend nie będzie mogła zostać uruchomiona, ponieważ CanSaveFileA lub CanSaveFileB zwróci false, przycisk pozostanie w stanie nieaktywnym. Podobnie jak z rejestracją, dowolna komenda może również zostać odrejestrowana:

SaveAllCmd.UnregisterCommand(SaveFileBCmd);

Ze względu na globalne użycie CompositeCommands, dobrym zwyczajem jest wyeksponowanie klasy w jednym globalnym miejscu, np. w statycznej klasie:

public static class AllCommands
{
        public static CompositeCommand SaveAllCmd = new CompositeCommand();
}

Następnie wiązanie definiowane jest następująco:

<Button Command="{x:Static cmds:AllCommands. SaveAllCmd }">Save all</Button>

Powyższe rozwiązanie pozwoli na wykorzystanie komendy w różnych ViewModel.

Niestety statyczne klasy mają swoje wady i programiści niechętnie je wykorzystują. Podstawowym problemem jest utrudnienie podczas implementacji zautomatyzowanych testów. Aby umożliwić wykorzystanie Mock’ów, podczas testów jednostkowych, warto stworzyć dodatkowe Proxy:

public class AllCommandsProxy
    {
        public virtual CompositeCommand SaveAllCmd
        {
            get { return AllCommands.SaveAllCmd; }
        }
    }

W razie konieczności można przeładować AllCommandsProxy i wstawić własny Mock.

Composite Event

Zdarzenia odpowiedzialne są za wymianę informacji między modułami (np. między różnymi ViewModel), w przeciwieństwie do komend, które służą do komunikacji View – ViewModel. Mechanizm zdarzeń oparty jest na zasadzie subskrypcji oraz publikacji. Jeden z ViewModel’i, który eksponuje zdarzenie, dokonuje publikacji. Następnie wszystkie inne klasy, które dokonały subskrypcji, zostaną o tym powiadomione. Każde zdarzenie może przekazać własny zestaw parametrów.

Zdefiniuj więc proste zdarzenie o parametrze typu string:

  1. Aby zadeklarować Composite Event, należy skorzystać z klasy CompositePresentationEvent:
CompositePresentationEvent<string> _sampleEvent = null;

W powyższym przykładzie typem parametru jest string.

  1. Inicjalizacja niczym nie różni się od stworzenia zwykłej klasy:
_sampleEvent=new CompositePresentationEvent<string>();
  1. Następnie (w innych klasach) wykonaj subskrypcję:
_sampleEvent.Subscribe(EventHandler1);
    _sampleEvent.Subscribe(EventHandler2);
  1. Zadeklaruj metody EventHandler1 oraz EventHandler2:
private void EventHandler1(string text)
    {
        MessageBox.Show(string.Format("1:{0}", text));
    }
    private void EventHandler2(string text)
    {
        MessageBox.Show(string.Format("2:{0}", text));
    }
  1. Wykonaj publikację. Jeśli chcesz zakomunikować subskrybentom, wywołaj metodę Publish oraz przekaż parametr:
_sampleEvent.Publish("Hello world!");

Po publikacji, metody EventHandler1 oraz EventHandler2 zostaną wywołane z parametrem „Hello world”.

Dobrym zwyczajem jest stworzenie osobnych klas dla zdarzeń, definiując w nich od razu typ parametrów:

public class OrderEvent : CompositePresentationEvent<Order>
{
}
public class Order
{
    // jakieś pola
}
OrderEvent _sampleEvent = new OrderEvent();

Prosty zabieg zwiększa czytelność kodu i zmniejsza szanse na popełnienie błędu podczas deklaracji lub inicjalizacji. Jednak znaczącą korzyścią wynikającą z tego podejścia, jest możliwość wykorzystania agregacji zdarzeń.

Event Aggregation

EventAggregation jest klasą, dostarczoną przez PRISM, która służy do zarządzania instancjami zdarzeń. Zamiast samodzielnie tworzyć i przechowywać CompositeEvent, możesz wykorzystać rozwiązanie dostarczone przez PRISM. EventAggregation stanowi zatem kontener dla zdarzeń. Wykorzystując kontener w aplikacji, jedyną wymaganą instancją jest EventAggregation. Wszystkie zdarzenia nie muszą być dostępne publicznie, ponieważ dostęp do nich będzie realizowany za pomocą EventAggregation.

Aby utworzyć instancję wcześniej zdefiniowanego zdarzenia OrderEvent, zastosuj metodą Get<T>:

OrderEvent orderEvent = _aggregator.GetEvent<OrderEvent>();
orderEvent.Subscribe(EventHandler1);
orderEvent.Subscribe(EventHandler2);

Inicjalizacja agregatora również nie jest skomplikowana:

private EventAggregator _aggregator = new EventAggregator();

Przykład publikacji:

_aggregator.GetEvent<OrderEvent>().Publish(new Order());

Warto również zaznaczyć, że zdarzenia można wyrejestrować za pomocą token’a, zwracanego przez Subscribe:

SubscriptionToken token= orderEvent.Subscribe(EventHandler2);
orderEvent.Unsubscribe(token);

Zakończenie

Komendy oraz zdarzenia pozwalają na luźne powiązania między widokiem a ViewModel oraz między poszczególnymi ViewModel’ami. PRISM dostarcza kilku mechanizmów, które programiści musieli wcześniej zazwyczaj samodzielnie zaimplementować. Oprócz sposobu wykorzystania API, z artykułu warto jednak zapamiętać różnice pomiędzy zdarzeniami a komendami, bowiem programiści często zapominają o nich. Zdarzenia służą do komunikacji, powiadomienia o jakieś akcji i nie mogą być stosowane do wykonywania poleceń na podstawie interakcji użytkownika z interfejsem – do tego służą komendy, które mają dodatkowo specjalną właściwość CanExecute. Innymi słowy, komenda oznacza „zrób coś!”, z kolei zdarzenie oznacza to, że „akcja właśnie została wykonana”.

 


     

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.