środa, 30 listopada 2011

MVVM i okna modalne

Często podczas użytkowania programów zachodzi konieczność pokazania dodatkowego okna, służącego do interakcji z użytkownikiem. Najczęściej okno takie należy pokazać w określonym przypadku. Jak już wcześniej zostało wspomniane cała logika powinna być umieszczona w ViewMoedlu, zatem nasuwa się pytanie, w jaki sposób z ViewModelu pokazać okno modalne - przecież ViewModel nie powinien mieć żadnych informacji o widoku. Rozwiązanie tego problemu polega na oddelegowaniu pokazywania widoków lub MessageBox-ów do wyspecjalizowanych klas tzw. serwisów. Ponadto w przypadku gdy chcemy pokazywać proste komunikaty możemy również skorzystać z Interaction/Interactivity oraz klasy Prisma InteractionRequest<T>.
1. InteractionRequest<T>
Jak już wcześniej wspomniano klasa InteractionRequest<T> odpowiedzialna jest za pokazywanie komunikatów użytkownikowi - koordynuje ona działanie pomiędzy ViewModelem a widokiem. W celu skorzystania z klasy InteractionRequest w ViewModelu tworzymy publiczną właściwość udostpęniającą obiekt typu InteractionRequest na zewnątrz

public InteractionRequest<Confirmation> Request { get; set; }
Request = new InteractionRequest<Notification>();


Jak widać za generyczny typ T została podstawiona klasa Notification. Parametr T może przyjmować klasy dziedziczące po klasie Notification. Domyślnie w Prismie są dwie takie klasy:

  • Notification - wspomniana już wcześniej, służy do powiadomienia użytkownika o jakimś zdarzeniu

  • Confirmation - również służy powiadominiu użytkownika o jakiś zdarzeniu, ale zwraca ona reakcję użytkownika (czy potwierdził daną akcję czy nie - coś jak MessageBox YesNo)


W celu wywołania okienka należy odpalić funkcję Raise na przykład w taki sposób:

Request.Raise(new Notification { Content = "Info dla użytkownika", Title = "sth" }, confirmation => AfterMessageAction());

Funkcja Raise przyjmuje jeden lub dwa parametry. Przedstawy bardziej zaawansowaną wersję tej funkcji - tą z dwoma parametrami. W pierwszym parametrze podajemy obiekt typu Notification, w którym przekazujemy informacje do przekazania użytkownikowi (Content) oraz tytuł okna (Title). Drugim parametrem jest delegat Action<Notification> , który określa co mamy zrobić po zamknięciu okna przez użytkownika.
Ostatnią rzeczą jaką musimy zrobić, żeby wykorzystać NotificationRequest jest zdefiniowanie triggera w widoku, który będzie reagował na odpalenie funkcji Raise. Robimy to w następujący sposób:

  • definiujemy w XAML-u alias do namespaca interactivity

    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"


  • tworzymy InteractionRequestTrigger

    <i:Interaction.Triggers>
    <prism:InteractionRequestTrigger SourceObject="{Binding Request}">
    <prism:PopupChildWindowAction></prism:PopupChildWindowAction>
    </prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>



2. MessageBoxService
Alternatywą dla InteractionRequest<T> jest stworzenie własnej klasy, do której zostałyby oddelegowane wszystkie prośby o pokazania MessageBoxa. Przykładowa implementacja takiego serwisu może wyglądać w następujący sposób. Po pierwsze stwórzmy interfejs z funkcjami potrzebnymi do pokazania MessageBoxa

public interface IMessageBoxService
{
MessageBoxResult ShowMessageBox (string info, string caption,MessageBoxButton buttons);
}

Następnie zaimplementujmy ten interfejs w klasie MessageBoxService - właśnie do tej klasy będą oddelegowywane wszystkie prośby o pokazanie MessageBoxa

public class MessageBoxService : IMessageBoxService
{

public MessageBoxResult ShowMessageBox(string info, string caption, MessageBoxButton buttons)
{
return MessageBox.Show(info, caption, buttons);
}
}

Taki serwis musimy następnie przekazać do każdego ViewModelu , w którym będzie potrzeba pokazania MessageBoxa. Jako, że ręczne przekazywanie tego obiektu może być nużące, możemy skorzystać z dobrodziejstw Inversion of Control. Ja skorzystałem z kontenera z Unity. Po pierwsze musimy zarejestrować nasz typ w kontenerze

_container.RegisterType<IMessageBoxService, MessageBoxService>();

następnie MessageBoxService może zostać wstrzyknięty do każdego ViewModelu przy np. przy pomocy wstrzykiwania właściwości.

[Dependency]
public IMessageBoxService MessageBoxService
{
get { return messageBoxService; }
set { messageBoxService = value; }
}

Oczywiście żeby nasza właściwość została wstrzyknięta, nasz ViewModel musi zostać stworzony przy użycia kontenera IO
Zauważmy, że właściwość MessageBoxService nie jest konkretnym typem, lecz interfejsem. Dzięki takiemu podejściu nasza aplikacja wciąż jest w łatwy sposób testowalna oraz ViewModel nie ma nic wspólnego z klasami widoku.
3. ModalWindowService
Do pokazywania widoków(jako okien) z poziomu ViewModelu posłużmy się podobnym mechanizmem jak w przypadku pokazywania MessageBox-ów. Zasada działania klasy, która stworzymy będzie taka sama, jednakże będziemy musieli odrobinę poszerzyć jej funkcjonalność. Po pierwsze tak jak poprzednio stwórzmy odpowiedni interfejs - IModalWindowService.
UWAGA
Przykład ModalWindowServicu zostanie zaprezentowany dla Silverlight-a, gdyż okna modalne w silerlighcie to tak naprawdę okna semi-modalne. Główny wątek się nie zatrzymuje, jednakże użytkownik nie ma możliwości interakcji z oknami będącymi pod aktualnie widocznym oknem.


public interface IModalDialogService
{
void ShowDialog<TViewModel>(IModalView<TViewModel> view,Action<TViewModel> CloseAction) where TViewModel : ViewModelBase;
}

Zauważmy, że funkcja ShowDialog w parametrze nie przyjmuje konkretnego widoku, lecz interfejs, który każde okno modalne powinno implementować - IModalView. Interfejs ten może wyglądać w następujący sposób:

public interface IModalView<TViewModel> where TViewModel : ViewModelBase
{
///
/// ViewModel odpowiedzialny za dane okno
///

TViewModel ViewModel { get; set; }
///
/// DialogResult po zamknieciu okna
///

bool? DialogResult { get; set; }
///
/// zamkniecie okna
///

void Close();
///
/// Pokazanie okna
///

void Show();
///
/// Zdarzenie wywoływane w momencie zamkniecia okna modalnego
///

event EventHandler Closed;
///
/// Zdarzenie wywoływane w chwli zamykania widoku
///

event EventHandler<CancelEventArgs> Closing;

}

Jako, że okna modalne w Silverlighcie są tak naprawdę oknami semi-modalnymi (patrz wytłumaczenie w sekcji UWAGA) musimy wiedzieć kiedy dane okno zostanie zamknięte. Dlatego, też interfejs ten zawiera zdarzenie

event EventHandler Closed;

ponadto wypadałoby sprawdzić czy dane wpisane w oknie są poprawne, dlatego też dodano zdarzenie

event EventHandler<CancelEventArgs> Closing;

w przypadku gdy dane wpisane w oknie nie będą poprawne, nasz ModalDialogService zapobiegnie zamknięciu okna. Ostatecznie nasza klasa ModalDialogService może wyglądać w następujący sposób:

public class ModalDialogService : IModalDialogService
{
private EventHandler<CancelEventArgs> ClosingEventHandler;
private EventHandler CloseEventHandler;
public void ShowDialog<TViewModel>(IModalView<TViewModel> view, Action<TViewModel> CloseAction) where TViewModel : ViewModelBase
{

view.Closing += ClosingEventHandler = (sender, args) =>
{
if (view.DialogResult.HasValue && view.DialogResult.Value)
{
if (!(args.Cancel = !view.ViewModel.Validate())) // jezeli mozna zamknac okno to odczepiamy handler
view.Closing -= ClosingEventHandler;
}
};
if (CloseAction != null)
{
view.Closed += CloseEventHandler = (sender, args) =>
{
CloseAction(view.ViewModel);
view.Closed -= CloseEventHandler;
};
}
view.Show();
}
}

Nasz serwis podobnie jak w przypadku MessageBoxServicu rejestrujemy w kontenerze, a następnie wstrzykujemy go do ViewModelu

_container.RegisterType<IModalDialogService, ModalDialogService>();
[Dependency]
public IMessageBoxService MessageBoxService
{
get { return messageBoxService; }
set { messageBoxService = value; }
}

Pozostaje nam jedynie kwestia wyjaśnienia w jaki sposób wywołać nasze okno. Nie powinniśmy tworzyć okna w ViewModelu, gdyż łamie to zasadę MVVM-a. Dlatego też po raz kolejny skorzystamy z kontenera IoC. W kontenerze IoC rejestrujemy sobie nasz widok

_container.RegisterType<IModalView<AddPersonViewModel>,AddPersonView>()

jako, że ViewModel jest wstrzykiwany do View poprzez Dependency Injection należy również w kontenerze zarejestrować odpowiedni typ ViewModelu - czyli w tym przypadku AddPersonViewModel

_container.RegisterType<AddPersonView>()

Mając już zarejestrowane w kontenerze wszystkie niezbędne typu możemy wywołać okno z ViewModelu w następujący sposób:

ModalDialogService.ShowDialog<EditAllPlayersViewModel>(_container.Resolve<IModalView<AddPersonViewModel>>(), model => {//tutaj jakas logika });

Obiekt widoku otrzymujemy z kontenera IoC wykorzystując funkcję Resolve, w miejsce LambdaExpression wstawiamy nasze wyrażenie w którym określamy co zrobimy po poprawnym zamknięciu okna. Zauważmy, że ViewModel nie ma żadnych informacji o widoku - kontener nie zwraca nam konkretnego typu lecz interfejs. Dzięki takiemu podejściu nasza aplikacja może być w łatwy sposób testowalna.

wtorek, 29 listopada 2011

Prism cz. 6 - Interactivity oraz Interaction

Interactions oraz Interactivity są to dwie dll-ki, które poszerzają sposób komunikacji pomiędzy widokiem oraz ViewModelem. Wprowadzają one nowy typ EventTriggerów(w Silverlighcie nie ma triggerów, natomiast w WPF-ie event triggery można praktycznie używać jedynie do animacji) oraz dodatkowo wprowadzają ciekawe sposoby(funkcje) na interakcję widoku z ViewModelem. Pierwszym z tych sposobów jest użycie właściwości InvokeCommandAction.Jak sama nazwa wskazuje można wywołać komendę z ViewModelu. Jaka jest różnica między wywołaniem komendy poprzez InvokeCommandAction, a Command="{Binding CommandName}" ? InvokeCommandAction ma tą przewagę nad normalnm bindowaniem do property Command,że pozwala wywołać daną komendę w odpowiedzi na zajście jakiegoś zdarzenia na widoku. Przykładowe użycie może wyglądać następująco:

<Button Content="Submit" IsEnabled="{Binding CanSubmit}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove">
<i:InvokeCommandAction Command="{Binding SubmitCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>

Powyższy przykład działa w następujący sposób. Za każdym razem gdy zostanie wywołane zdarzenie MouseMove na przycisku, odpalona zostanie komenda SubmitCommand. Wadą używania InvokeCommandAction jest to, że sami musimy się martwić o wygaszenie kontrolki w przypadku gdy dana komenda nie może być wykonana. Dlatego też w pierwszej linii mamy bindowanie właściwości IsEnabled przycisku do właściwości CanSubmit z ViewModelu. Alias i: jest aliasen na namespace interactivity z dll-ki

System.Windows.Interactivity.xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity".


Drugim sposobem powiadomiania ViewModelu o jakimś zdarzeniu w widoku jest użycie InvokeMethodAction. Dzięki tej właściwości możemy bezpośrednio z widoku (z XAML-a) wywołać funkcję w ViewModelu. Składnia przedstawia się w następujący sposób:

<Button Content="Submit" IsEnabled="{Binding CanSubmit}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:CallMethodAction TargetObject="{Binding}" Method="Submit"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>

TargetObject jest zbindowany do DataContextu, to właśnie tam będzie szukana funkcja Submit. Należy pamiętać, że właściwość CallMethodAction nie wspiera parametrów funkcji. Zatem jeżeli musimy wywołać jakąś funkcję z parametrami, należy opakować ją w funkcję bezparametrową, natomiast wszystkie parametry przekazać z właściwości ViewModelu.

Kolejnym ciekawym featurem Interactivity jest klasa Behavior<T>. Jest niezastąpiona jeżeli potrzebujemy ingerować w działanie widoku po jakimś zdarzeniu. Nie musimy wówczas tworzyć obsługi eventów w code behind. Możemy wyłuskać daną funkcjonalność i zamknąć ją w klasie Behavior a następnie doczepić do wszystkich widoków danego typu. Załóżmy, że sterujemy ListBoxem z poziomu ViewModelu (przełączamy SelectedItem). Możemy zaznaczyć jakiś Item, co jednak jeżeli nasza lista jest bardzo długa,a zaznaczamy ostatni element. Na widoku element rzeczywiście się zaznaczył jednak lista się nie przewinęła. Przez co cały czas wiszą elementy z początku i nie widać czy któryś element jest zaznaczony. Dopiero gdy przeskrolujemy ręcznie ListBoxa widzimy, że rzeczywiście prawidłowo zareagował na zmianę property w ViewModelu. Jest to idealna sytuacja, aby stworzyć własny Behavior, który będziemy mogli podczepić do wszystkich listboxów w każdym widoku.

public class Behavior<ListBox>
{
protected override void OnAttached()
{
AssociatedObject.SelectionChanged += ListBoxSelectionChanged;
}

protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= ListBoxSelectionChanged;
}

private void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox && ((ListBox)sender).SelectedItem != null)
{
var listBox = sender as ListBox;
listBox.ScrollIntoView(listBox.SelectedItem);
}
}
}

Metoda OnAttached odpala się w momencie, gdy behavior jest podpinany do kontrolki. W metodzie tej powinniśmy podpiąć się pod wszystkie eventy, które są dla nas istotne. W momencie, gdy widok jest niszczony wywołuje się metoda OnDetaching, w której powinniśmy odpiąć wszystkie metody od eventów.

Prism cz.5 - CommandBehaviors

Analizując wcześniejsze przykłady pokazujące użycie DelegateCommand można odnieść wrażenie, że mimo tego iż są one bardzo użyteczne,ich użycie jest niestety bardzo ograniczone.
Jedynie niektóre komponenty WPF-a i Silverlight-a mają właściwość Command do której można zbindować nasz obiekt DelegateCommand.Ponadto właściwość ta reaguje jedynie na wybrane zahardcodowane w kontrolce zdarzenie (zdarzenie Click). Co w przypadku gdybyśmy chcieli zareagować np. na zdarzenie MouseMove za pomocą komendy? Nasz problem możemy rozwiązać poprzez:

  • Interactivity

  • Interactions

  • CommandBehaviors


W tym poście zostanie przedstawione rozwiązanie trzecie - CommandBehaviors

Pierwszą rzeczą jaką należy zrobić w celu stworzenia komendy reagującej na inne zdarzenie niż Click jest stworzenie klasy dziedziczącej po CommandBehaviorBase. Załóżmy, że zrobimy komendę reagującą na zdarzenie MouseMove

public class MouseMoveCommandBehavior : CommandBehaviorBase<Control>
{
private MouseOutCommandBehavior(Control control) : base(control)
{
control.MouseMove += (sender, args) =>
{
this.ExecuteCommand();
};
}
}

Piersza część już za nami, ale pojawia się teraz pytanie jak z tego skorzystać. Żeby mieć możliwość użycia powyżej klasy wykorzytamy attached dependency property. Musimy sobie stworzyć pomocniczą statyczną klasę, w której to zarejestrujemy nasze właściwości. Zacznijmy od zarejestrowania dependency property o nazwie Command - do tego obiektu będziemy bindowali komendy z ViewModelu (komendy reagujące na zdarzenie MouseMove)

public static class MouseMove
{

public static ICommand GetCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(CommandProperty);
}

public static void SetCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(CommandProperty, value);
}

public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(MouseMove), new PropertyMetadata(OnSetCustomCommandCallback));
}

zauważmy, że podłączyliśmy event handlera reagującego na zmianę naszej nowo stworzonej właściwości Command. Funkcja OnSetCustomCommandCallback wygląda następująco:

private static void OnSetCustomCommandCallback(DependencyObject dep, DependencyPropertyChangedEventArgs e)
{
var control = dep as Control;
if (control != null)
{
MouseMoveCommandBehavior behavior = GetorCreateMouseMoveBehavior(control);
behavior.Command = e.NewValue as ICommand;
}
}

Funkcja ta po prostu w razie potrzeby tworzy nowy obiekt typu MouseMoveCommandBehavior (gdy ktoś zbinduje komende) i dopina go do naszej kontrolki. Pomocnicza funkcja GetorCreateMouseMoveBehavior wygląda tak:

private static MouseMoveCommandBehavior GetorCreateMouseMoveBehavior(Control listBox)
{
var behavior = listBox.GetValue(MouseMoveBehaviorProperty) as
MouseMoveCommandBehavior;
if (behavior == null)
{
behavior = new MouseMoveCommandBehavior(listBox);
listBox.SetValue(MouseMoveBehaviorProperty, behavior);
}
return behavior;
}

Obiekt rejestrowany poprzez funkcję GetorCreateMouseMoveBehavior jest dopinany do kolejnej attached property, która również została zadeklarowana w tym pliku

public static MouseMoveCommandBehavior GetMouseMoveBehavior(DependencyObject obj)
{
return (MouseMoveCommandBehavior)obj.GetValue(MouseMoveBehaviorProperty);
}

public static void SetMouseMoveBehavior(DependencyObject obj, MouseMoveCommandBehavior value)
{
obj.SetValue(MouseMoveBehaviorProperty, value);
}
public static readonly DependencyProperty MouseMoveBehaviorProperty =
DependencyProperty.RegisterAttached("MouseMoveBehavior", typeof(MouseMoveCommandBehavior), typeof(MouseMove), null );

Mając zdefiniowane wszystkie powyższe właściwości możemy już wykorzystać naszą komendę. Jednakże przydałoby się dodać jeszcze jedną właściwość (opcjonalnie), mianowicie właściwość CommandParameters

public static object GetCommandParametr(DependencyObject obj)
{
return (object)obj.GetValue(CommandParametrProperty);
}

public static void SetCommandParametr(DependencyObject obj, object value)
{
obj.SetValue(CommandParametrProperty, value);
}

public static readonly DependencyProperty CommandParametrProperty =
DependencyProperty.RegisterAttached("CommandParametr", typeof(object), typeof(MouseMove), new PropertyMetadata(OnSetCustomCommandParameterCallback));

W tym propertisie również reagujemy na przypisnie obiektu.Handler OnSetCustomCommandParameterCallback wygląda następująco

private static void OnSetCustomCommandParameterCallback(DependencyObject dep,
DependencyPropertyChangedEventArgs e)
{
var listBox = dep as Control;
if (listBox != null)
{
MouseMoveCommandBehavior behavior = GetorCreateMouseMoveBehavior(listBox); ;
behavior.CommandParameter = e.NewValue;
}
}

Mając już zdefiniowane wszystkie niezbędne właściwości możemy użyć ich w XAML-u. Po pierwsze musimy dodać alias do namespaca gdzie znajduje się nasza statycza klasa MouseMove. W moim przypadku wygląda to następująco:

xmlns:AttachedCommands="clr-namespace:MainModule.Commands"

Następnie bindujemy jakąś kontrolkę do naszego attached property Command. Wygląda to w następujący sposób:

<Button Name="txtButton" Grid.Column="2" AttachedCommands:MouseMove.Command="{Binding SomeCommandFromViewModel}" ></Button>

Od teraz za każdym razem jak najedziemy na txtButton wywoła się komenda SomeCommandFromViewModel.
Podsumowując trzeba się całkiem sporo napisać żeby zmusić do działania CommandBehaviors . W następnym poście przedstawię dwa sposoby na osiągnięcie tego samego efektu mniejszym kosztem.

Prism cz.4 - Komendy

1. DelegateCommands
DelegateCommands w Prismie są to obiekty, które implementują interfejs ICommand, służą one do interakcji widoku z ViewModelem. Korzystając z Prisma nie musimy już pisać własnej klasy implementującej ten interfejs (tak jak to zrobiliśmy tutaj). Konstruktor klasy DelegateCommand przyjmuje dwa parametry:

  • Action execteMethod - funkcja odpalania przy wywołaniu komendy,

  • Func<bool> canExecuteMethod - funkcja sprawdzająca czy daną komendę można wywołać


DelegateCommands z Prisma używamy w taki sam sposób w jaki używaliśmy komend w tym poście.

2. CompositeCommands
CompositeCommands są to obiekty, które przechowują kolekcją obiektów typu DelegateCommand. Tworzenie CompositeCommand jest bardzo proste.
Po pierwsze musimy stworzyć obiekt typu CompositeCommand

public CompositeCommand SomeCompositeCommand = new CompositeCommand();

następnie rejestrujemy w tym obiekcie obiekt implementujący interfejs ICommand, czyli w naszym przypadku jakiś obiekt typu DelegateCommand. Robimy to w następujący sposób:

DelegateCommand DistributeCommand = new DelegateCommand(DistributeCards);
SomeCompositeCommand .RegisterCommand(DistributeCommand);

w razie potrzeby możemy usunąć komendę z kolekcji komend przy pomocy następującej składni:

SomeCompositeCommand .UnregisterCommand(DistributeCommand);

Mając już stworzony obiekt typu CompositeCommand oraz zarejestrowane w nim obiekty DelegateCommand możemy zastosować bindowanie.



Od teraz, w chwili naciśnięcia przycisku, najpierw sprawdzane jest czy dana CompositeCommand może zostać wykonana. CompositeCommand może zostać wykonana tylko wtedy, gdy wszystkie komendy do niej podpięte mogą zostać wykonane. Jeżeli warunek ten zostanie spełniony następuje wywoływanie po kolei wszystkich metod execute z komend podpiętych do danej CompositCommand. CompositeCommand znajduje zastosowanie w przypadku gdy pozwalamy użytkownikowi na edycję wielu elementów jednocześnie i potrzebujemy w prosty sposób przeprowadzić walidację na poszczególnych widokach.

CompositeCommand może również wykorzystana np. do zapisania tylko aktywnego widoku. Dzięki temu, że DelegateCommand implementuje interfejs IActiveAware, CompositeCommand wie, który widok jest aktualnie zaznaczony i zapisze tylko wybrany widok.

Interfejs IActiveAware wygląda w następujący sposób

public interface IActiveAware
{
bool IsActive { get; set; }
event EventHandler IsActiveChanged;
}


  • bool IsActive - wskazuje czy dana komenda jest aktualnie aktywna

  • event EventHandler IsActiveChanged - informuje o zmianie aktywności komendy


W celu wykorzystania tego interfejsu przez CompositeCommand należy skorzystać z drugiego dostępnego konstruktora tej klasy

CompositeCommand SomeCompositeCommand = new CompositeCommand(true);

Od teraz po naciśnięciu przycisku zbindowanego do tej komendy, wywołana zostanie jedynie aktywna komenda - czyli taka, której właściwość IsActive została ustawiona na true. Niestety o ustawianie aktywności danej komendy musimy zadbać sami.

Prism cz. 3 - Regiony

Regiony są podstawą aplikacji napisanych za pomocą Prism'a. Ułatwiają nawigację po programie i pozwalają na dynamiczną lokację widoków w naszej aplikacji. Regiony dzielą graficzny interfejs użytkownika na obszary, do których wczytywane są odpowiednie widoki. Wystarczy w kodzie wpisać że do Regionu A trzeba załadować Widok B. Prosty podział standardowej aplikacji na regiony może wyglądać tak:

W xaml'u wyglądałoby to mniej więcej w ten sposób:
<Grid ...>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="4*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="4*"/>
</Grid.RowDefinitions>
<ContentControl Regions:RegionManager.RegionName="LogoRegion" Grid.ColumnSpan="2"/>
<ContentControl Regions:RegionManager.RegionName="NavigatorRegion" Grid.Row="1"/>
<ContentControl Regions:RegionManager.RegionName="MainRegion" Grid.Column="1" Grid.Row="1"/>
</Grid>

Tak jak widać powyżej regiony definiujemy poprzez AttachedProperty RegionName klasy RegionManager. Od tej pory jeżeli w kodzie wywołamy następującą metodę:
regionManager.Regions["MainRegion"].Activate(view);

w obszarze "MainRegion" pojawi się nasz widok. Oznacza to tyle samo, że właściwość Content naszej ContentControl zostanie wypełniona obiektem "view". Oczywiście regiony mogą być zagnieżdżone jeden w drugim i Prism bez problemu poradzi sobie z taką sytuacją.

Powyższe rozwiązanie aktywacji widoku powinno być opakowane w jakiś obiekt pośredni. Nie zawsze bowiem mamy dostęp do obiektu widoku, w momencie gdy chcemy go aktywować. Naturalny wydaje się poniższy zapis:
ViewService.ActivateView(Type type);
ViewService.ActivateView(string viewName);

W pierwszym zapisie tak czy inaczej musimy mieć dostęp do typu widoku. Drugie rozwiązanie wydaje się bardziej elastyczne. Możemy je zaimplementować bez konieczności dołączania referencji do projektu z interfejsem graficznym. Przykładowy kod może wyglądać tak:
public void ActivateView(string viewName)
{
foreach (IRegion region in regionManager.Regions)
{
object view;
if (RegionContainsViewName(region, viewName, out view))
{
region.Activate(view);
break;
}
}
}

Na koniec mała wskazówka jeżeli chodzi o utrzymanie kodu. Nasz kod może być trudny w utrzymaniu, jeżeli np. chcemy zmienić nazwę regionu dlatego dobrą praktyką jest trzymanie nazw jako stałych. Jest to bardzo proste do rozwiązania w WPF'ie, gdzie możemy się odwoływać do stałych poprzez x:Static. Jeżeli chodzi o Silverlight warto zastosować pewne obejście:
<Grid.Resources>
<ResourceDictionary>
<Resources:Names x:Key="Names"/>
</ResourceDictionary>
</Grid.Resources>
...
<ContentControl Regions:RegionManager.RegionName="{Binding NavigatorRegion, Source={StaticResource Names}}"/>
<ContentControl Regions:RegionManager.RegionName="{Binding MainRegion, Source={StaticResource Names}}"/>

gdzie klasa Names może wyglądać następująco:
public class Names
{
public string MainRegion { get { return "MainRegion"; } }
public string NavigatorRegion { get { return "NavigatorRegion"; } }
}

Prism cz. 2: Moduły

Czym tak naprawdę jest moduł? Moduł jest to zbiór widoków i funkcji aplikacji, które można odseparować od reszty. Moduł może być równie dobrze jedynie serwisem bez graficznego interfejsu użytkownika. Poszczególne moduły powinny móc komunikować się pomiędzy sobą, jednak komunikacja nie powinna naruszać ich niezależności. Sam użytkownik nie powinien mieć świadomości modularności aplikacji, która jest dla niego spójna i łatwa w użyciu.
No dobra, ale po co pisać aplikacje modułowe? Jeżeli stworzyliśmy aplikację modułową zgodnie ze sztuką, wówczas jest ona łatwa do testowania, utrzymania i rozwijania w przyszłości. Można bezproblemu dodawać nowe funkcjonalności nie ingerując w istniejący kod i rozwiązania. Każdy moduł może być osobno testowany wdrażany i rozwijany, ponieważ jest niezależną jednostką.

Bardzo prostym przykładem aplikacji modułowej może być program .NetTurnGames. Jest to aplikacja, która gromadzi w sobie różnego rodzaju gry turowe. Użytkownik po odpaleniu programu wybiera którą grę chce uruchomić. Każda mała gra turowa jest oddzielnym modułem. Wówczas użytkownik w łatwy sposób mógłby dodawać kolejne moduły np. kopiując pliki z nową grą do odpowiedniego katalogu.
Gdybyśmy chcieli ograniczyć się jedynie do gier karcianych, wówczas możemy interfejs użytkownika zamknąć w osobnym module natomiast zasady i logika każdej gry były by osobnym modułem. Podział aplikacji na moduły nie jest trywialnym zadaniem i leży po stronie projektanta aplikacji. Z reguły nie ma sensu wyodrębniać oddzielnego modułu, który byłby osobnym projektem i zawierał tylko jeden plik, a w nim jedną klasę z jedną metodą. Takie postępowanie z reguły prowadzi do nadmiaru projektów i małej czytelności aplikacji.
Powyższy przykład programu .NetTurnGames, powinien mieć wczytywane poszczególne gry w czasie działanie programu (runtime). Można to rozwiązać za pomocą reflekcji - sprawdzać czy odpowiedni moduł implementuje niezbędny interfejs. Jest to przykład wzorca Plugin.
Jednak moduły nie muszą dzielić jedynie aplikacji wg funkcjonalności, równie dobrze można podzielić aplikację warstwową ze względu na warstwy czyli otrzymalibyśmy mniej więcej takie moduły: UIModule, BuisinessLogicModule, DataAccessModule. Duże aplikacje mogą łączyć w sobie oba podejścia czyli dzielić moduły wg funkcjonalności i warstw jednocześnie.
W kontekście .NET doskonałym framework'iem, który ułatwia pisanie takich aplikacji jest Prism. Prism wręcz wymusza na nas, abyśmy pisali aplikacje modułowe.
Zazwyczaj przyjmuje się, że każdy moduł jest oddzielnym projektem (nic jednak nie stoi na przeszkodzie, aby moduł składał się z wielu projektów, lub jeden projekt z wielu modułów).

Każdy moduł powinien zawierać klasę implementującą interfejs IModule, dzięki czemu możemy go zainicjalizować.
public class MyModule : IModule
{
public void Initialize(){}
}

Podczas inicjalizacji, tworzymy wszystkie obiekty niezbędne do poprawnego działania modułu oraz rejestrujemy widoki.
Wszystkie informacje nt. modułów trzymane są w obiekcie klasy ModuleCatalog, który znajduje się w bootstrapperze. Również w bootstrapperze jest metoda, którą musimy przeciążyć, aby dodać przy starcie aplikacji wszystkie potrzebne moduły.
protected override IModuleCatalog CreateModuleCatalog()
{
IModuleCatalog moduleCatalog = new ModuleCatalog();
moduleCatalog.AddModule(typeof(CardGameModule.CardGameModule));
return moduleCatalog;
}

W obiekcie typu ModuleCatalog każdy moduł reprezentowany jest przez obiekt klasy ModuleInfo.
Moduły można wczytywać od razu lub doczytywać je na żądanie. Domyślnie każdy wczytywany jest tak szybko jak to tylko możliwe, natomiast można to zmienić dodając do metody AddModule argument:
moduleCatalog.AddModule(typeof(CardGameModule.CardGameModule), InitializationMode.OnDemand);


Wówczas w momencie gdy chcemy załadować moduł musimy na obiekcie typu IModuleManager wywołać metodę LoadModule
moduleManager.LoadModule("CardGameModule");


Moduły można dodawać również za pomocą pliku xaml. Taka implementacja wygląda wówczas następująco:
<Modularity:ModuleCatalog
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">
<Modularity:ModuleInfoGroup Ref="CardGameModule.xap" InitializationMode="WhenAvailable">
<Modularity:ModuleInfo ModuleName="CardGameModule" ModuleType="CardGameModule.CardGameModule, CardGameModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</Modularity:ModuleInfoGroup>
</Modularity:ModuleCatalog>

protected override IModuleCatalog CreateModuleCatalog()
{
var moduleCatalog = Microsoft.Practices.Prism.Modularity.ModuleCatalog.CreateFromXaml(new Uri("/MVVMPrismSilverlightv3;component/ModulesCatalog.xaml", UriKind.Relative));
return moduleCatalog;
}

Ponieważ każdy moduł jest niezależną jednostką, zazwyczaj zachodzi potrzeba komunikacji pomiędzy modułami. Jeżeli moduły mają dostęp do tego samego interfejsu (typu) wówczas możemy wstrzyknąć do wielu różnych modułów ten sam obiekt danego typu. Proces wstrzykiwania opisuje pattern Dependency Injection (DI). W prismie możemy domyślnie korzystać z dwóch bibliotek wspierających DI: Unity oraz MEF, jednak nic nie stoi na przeszkodzie, aby podpiąć inną bibliotekę np StructureMap i wykorzystywać jej możliwości w projekcie opartym na PRISM (wykracza to poza temat tego posta). PRISM dostarcza nam konkretnego rozwiązania problemu komunikacji, opartego na patencie DI. Jest to EventAggregator. Jeżeli korzystamy z domyślnych klas PRISMA nie musimy rejestrować EventAggregator'a ponieważ jest on rejestrowany w bazowej metodzie ConfigureContainer() naszego bootstrappera.

Źródło wiedzy:

Developer Guide to Microsoft Prism 4

poniedziałek, 28 listopada 2011

ListViewCollection oraz PagedCollectionView

1. ListViewCollection (WPF)
ListViewCollection jest to kolekcja wprowadzona do WPF-a, która wspiera sortowanie, grupowanie oraz filtrowanie. Kolekcję taką tworzymy w następujący sposób

List<Player> palyerList = new Player { new Player(),new Player()};
public ListViewCollection<Player> PlayerListCollectionView {get;set;}
ListViewCollection<Player> PlayerListCollectionView= new ListViewCollection<Player>(playerList);

czyli po prostu w konstruktorze przesyłamy obiekt implementujący interfejs IEnumerable
Filtrowanie
W celu przefiltrowania kolekcji wystarczy podać odpowiedni obiekt typu Predicate<object>(czyli tak naprawdę wystarczy napisać funkcję zwracającą bool oraz przyjmującą w parametrze obiekt). Załóżmy, że chcemy pokazać na widoku jedynie tych graczy, który nazywają się "Tomek". W tym celu wystarczy, że zrobimy coś takiego:

PlayerListCollectionView.Filter = val => { return (val as Player).Name.ToLower() == “tomek”; }

po zaaplikowaniu predicata widok zbindowany do danej listy automatycznie się odświeży. W razie czego można zawsze wymusić oświeżenie przez wywołanie funkcji

PlayerListCollectionView.Refresh();

Grupowanie
Kolekcja ListViewCollection wspiera również grupowanie. W celu pogrupowania listy po jakimś elemencie wystarczy do naszej listy dorzucić tzw. group description. Odbywa się to w taki sposób:

PlayerListCollectionView.GroupDescriptions.Add(
new PropertyGroupDescription { PropertyName = "Name" }

Po dodaniu PropertyGroupDescription widok automatycznie pogrupuje nam elementy po danych właściwościach.
Sortowanie
Nasza kolekcja wspiera również sortowanie. W celu posortowania wystarczy stworzyć obiekt implementujący interfjes IComparer, a następnie przypisać go do właściwości CustomSort

public class PlayerComparer : IComparer
{
public int Compare(object x, object y)
{
if (((Player)x).Age > (((Player)y).Age))
{
return 1;
}
return -1;
}
}
PlayerListCollectionView.CustomSort = new PlayerComparer ()

po takiej akcji widok automatycznie się odświeży i pokaże posortowane dane.
2. PagedCollectionView (Silverlight)
PagedCollectionView jest to uboższa wersja ListCollectionView z WPF-a. Grupowanie oraz filtrowanie dobywa się w ten sam sposób, jednakże sortowanie zostało w znaczny sposób utrudnione. Sortować możemy jedynie po właściwościach, nie możemy stworzyć własnego comparera, który zostałby użyty do wykonania operacji sortowania. Wykonanie operacji może wyglądać w następujący sposób:

PlayerPagedCollection.SortDescriptions.Clear();
PlayerPagedCollection.SortDescriptions.Add(new System.ComponentModel.SortDescription("Name", ListSortDirection.Ascending));

W tym wypadku nasza kolekcja zostanie posortowana po właściwości "Name"

WPF: Tworzenie własnych kontrolek

Tworzenie własnych kontrolek w WPF jest z pozoru łatwym zadaniem, klikamy Add-> new UserControl-> i wrzucamy do Contentu nowo powstałej kontrolki co nam się tylko podoba :) kontrolka działa, jest piękna i wszystko jest git. Nie jest to jednak najlepsze rozwiązanie.

Wady:
- w 99% przypadków stworzymy "ciężki" obiekt, który ma masę funkcjonalności do niczego nie potrzebnych.
- zaszywamy całą logikę w code-behind (odwołania przez Name do elementów w xaml'u). Co się stanie gdy musimy zmienić coś w xamlu, np podmienić ListBox'a na Grida. Nagle połowa naszych metod, które odwoływały się do tego ListBoxa musi zostać usunięta i napisana od nowa (trudniej skalowalny komponent).

Zalety:
- Często szybsza implementacja.

Na szczęście Microsoft zaopatrzył nas w całą gamę klas, które mogą nam posłużyć za bazę naszego komponentu (FrameworkElement, Shape, Control, ContentControl itd...). Po pierwsze trzeba wyizolować podstawową funkcjonalność tworzonego komponentu i wybrać odpowiednią klasę, która posłuży nam za bazę. Po drugie tworzymy zwykłą klasę nie "xamlową", która dodaje nasze niestandardowe funkcje do klasy bazowej. Na samym końcu na podstawie naszej nowej klasy możemy stworzyć xamla, któremu definiujemy Templata.

Przykład
public class Stupid : Control
{
public static readonly DependencyProperty StupidTextProperty =
DependencyProperty.Register("StupidText", typeof(string), typeof(Stupid));
public string StupidText
{
get { return (string)GetValue(StupidTextProperty); }
set { SetValue(StupidTextProperty, value); }
}
}

<WpfApplication2:Stupid x:Class="WpfApplication2.ConcreteStupid"
...>
<WpfApplication2:Stupid.Template>
<ControlTemplate TargetType="WpfApplication2:Stupid">
<TextBlock Text="{Binding StupidText, RelativeSource={RelativeSource
Mode=TemplatedParent}}"/>
</ControlTemplate>
</WpfApplication2:Stupid.Template>
</WpfApplication2:Stupid>

Ten przykład jest ekstremalnie prosty i ma za zadanie jedynie przedstawić szkic rozwiązania.
W przypadku, gdy zależy nam na stworzeniu bardzo lekkiego obiektu, chcemy mieć wpływ na to jak jest renderowany i chcemy stworzyć coś od zupełnych podstaw, warto się zastanowić nad klasą FrameworkElement jako bazą naszej kontrolki.
Proces tworzenia kontrolek jest świetnie opisany w książce "WPF Control Development Unleashed".

Walidacja

Walidacja jest to technika sprawdzająca, czy dany obiekt spełnia pewne założenia poprawności danych. W WPF-ie oraz Silverlighcie istnieją trzy sposoby walidacji obiektów:

  • walidacja poprzez rzucanie wyjątków,

  • walidacja z użyciem interfejsu IDataErrorInfo,

  • walidacja z użyciem interfejsu INotifyDataErrorInfo


1. Walidacja poprzez rzucanie wyjątków
Walidacja poprzez rzucanie wyjątków odbywa się w następujący sposób. W seterze danej właściwości dodajemy warunek sprawdzający czy wpisane dane są poprawne. Jeżeli nie to najzwyczajniej w świecie rzucamy wyjątek, w którym podajemy komunikat błędu. Przykładowy properties z walidacją może wyglądać w ten sposób:

private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
if (string.IsNullOrEmpty(_name))
throw new Exception("Nazwa nei może być pusta");
}
}

W celu "wyłapania" tego wyjątku i pokazania odpowiedniego komunikatu,w bindingu musimy ustawić właściwość ValidatesOnExceptions na wartość true.

<TextBox Text="{Binding Name,ValidatesOnExceptions=True,Mode=TwoWay}" />

Taki sposób walidowania jest jednak rzadko stosowany i wielu programistów twierdzi, że rzucanie wyjątków powinno się odbywać tylko w przypadku nieprawidłowego działania aplikacji. Ponadto walidowane propertisy nie mogą być autopropertisami, co dodatkowo wydłuża czas tworzenia klas.
2. Walidacja z użyciem interfejsu IDataErrorInfo

public interface IDataErrorInfo
{
string this[string columnName] { get; }
string Error { get; }
}

W celu wyłapywania błędów w widoku, należy w bindingu ustawić ValidatesOnDataError = true

<TextBox Text="{Binding Name,ValidatesOnDataError=True,Mode=TwoWay}" />

Przykładowa klasa implementująca interfejs IDataErrorInfo może wyglądać następująco:

public class Customer : IDataErrorInfo
{
public string Name { get; set; }
public string Error
{
get { return string.Empty; }
}

public string this[string propertyName]
{
get
{
string result = string.Empty;
if (propertyName == "Name")
{
if (string.IsNullOrEmpty(Name))
result = "Wartość nie moż być pusta";
}

return result;
}
}
}

Najważniejszą metodą w powyższej klasie jest indekser

string this[string propertyName]

to właśnie tutaj mogą zostać sprawdzone wszystkie właściwości danego obiektu - propertyName oznacza nazwę propertisu, który walidujemy. W przypadku, gdy wartość jakiejś właściwości jest nieprawidłowa, w pole result wpisujemy komunikat błędu. Komunikaty te "wyłapywane" są przez widok, a następnie wyświetlane w postaci komunikatów przy odpowiednich kontrolkach. Jeżeli wszystko jest OK zwracamy string.Empty.Walidacje przy pomocy interfejsu IDataErrorInfo idealnie nadają się do walidowania modelu.
3. Walidacja z użyciem interfejsu INotifyDataErrorInfo
Interfejs INotifyDataErrorInfo prezentuje się w następujący sposób:

public interface INotifyDataErrorInfo
{
bool HasErrors { get; }
event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
IEnumerable GetErrors(string propertyName);
}


  • bool HasErrors - określa czy dany obiekt zawiera błędy

  • event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged- zdarzenie informujące o zmienie ilości błędów w obiekcie

  • IEnumerable GetErrors(string propertyName) - funkcja pobierająca kolekcję błędów dla danego propertisa


W celu "wyłapania" błędów przez widok należy w bindingu ustawić properties
NotifyOnValidationError = true

<TextBox Text="{Binding Name,NotifyOnValidationError=True,Mode=TwoWay}" />

Jak już wcześniej wspomniano funkcja GetErrors(string propertyName) zwraca kolekcję błędów dla danej właściwości. Zatem do klasy, która będzie implementowała interfejs INotifyDataErrorInfo należy dodać kolekcję przechowującą obiekty typu ValidationResult. Przykładowa implementacja interfejsu może wyglądać w następujący sposób:

public class BaseViewModel : INotifyDataErrorInfo
{
private ICollection<ValidationResult> _validationResults;
public IEnumerable GetErrors(string propertyName)
{
return _validationResults.Where(result => result.MemberNames.Contains(propertyName));
}

public bool HasErrors
{
get { return _validationResults.Count > 0; }
}

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

private void NotifyErrorsChanged(string propertyName)
{
if(ErrorsChanged!=null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}

protected bool Validate()
{
_validationResults.Clear();
Validator.TryValidateObject(this, new ValidationContext(this, null, null), _validationResults, true);

foreach (var result in _validationResults)
NotifyErrorsChanged(result.MemberNames.First());

}

protected bool Validate(string propertyName)
{
var validationResults = _validationResults.Where(result => result.MemberNames.Contains(propertyName)).ToList();
foreach (var result in validationResults)
_validationResults.Remove(result);

Validator.TryValidateProperty(value, new ValidationContext(this, null, null) { MemberName = propertyName }, _validationResults);
NotifyErrorsChanged(propertyName);

}

Funkcja Validate() najpierw czyści wszystkie poprzednie wyniki walidacji, a następnie przy pomocy klasy Validator oraz funkcji TryValidateObject waliduje wszystkie właściwości, które zostały oznaczone atrybutem dziedziczącym po klasie ValidationAttribute.Z kolei funkcja Validate(string propertyName) waliduje tylko konkretną właściwość.
Przykładowe walidowanie właściwości przy pomocy atrybutów może wyglądać w następujący sposób:

[Required(ErrorMessage = "Pole nie może być puste")]
public string Subject
{
get { return _subject; }
set
{
_subject = value;
Validate(“Subject”)
}
}

W celu stworzenia własnych regół walidacji, nie uwzględnionych w zapewnionych przez framework atrybutach należy stworzyć własną klasę dziedziczącą po klasie ValidationAttribute, a następnie przeciążyć metodę IsValid. Przykładowa klasa może wyglądać w następujący sposób:

public class IntValidation : ValidationAttribute
{
private bool allowNull;
public IntValidation(bool allowNull)
{
this.allowNull = allowNull;
}

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null)
{
int resul;
if (int.TryParse(value.ToString(), out resul))
return ValidationResult.Success;
else
return new ValidationResult(ErrorMessage, new List{validationContext.MemberName });
}

return allowNull ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}

Walidacja przy użyciu interfejsu INotifyDataErrorInfo idealnie nadaje się (wg mnie) do walidowaniu całych ViewModeli. Przy zamykaniu okna wystarczy wywołać funkcję Validate() z bazowego ViewModelu, a w przypadku gdy zwróci ona false zatrzymać zamykanie okna. Jako, że walidacja zostanie przeprowadzona na wszystkich wybranych przez nas propertisach, widok automatycznie się zaktualizuje i pokaże komunikaty błędów na odpowiednich kontrolkach okna.

Routed Events - nowy rodzaj zdarzeń w WPF-ie

RoutedEvent jest to nowy typ zdarzeń, który po raz pierwszy został zaprezentowany w WPF-ie. Głównym założeniem RoutedEventów jest to, że w momencie wywołania takiego zdarzenia może ono podróżować w górę, lub w dół drzewa wizualnego oraz drzewa logicznego. Każdy RoutedEvent może przyjmować jedną z trzech strategii poruszania się po drzewie:

  • Bubbling- zdarzenie najpierw jest wywoływane w elemencie źródłowym, a następnie podróżuje ono w górę drzewa wizualnego (od naszego elementu do korzenia drzewa), aż do roota (lub do momenty gdy nie zostanie obsłużone poprzez e.Handled = true)

  • Tunelling - zdarzenie wywoływane jest w korzeniu drzewa, a następnie podróżuje w dół drzewa, aż osiągnie element źródłowy (lub gdy nie zostanie obsłużone poprzez e.Handled = true)

  • Direct - zdarzenie jest wywoływane tylko i wyłącznie w elemencie źródłowym - czyli zdarzenie to zachowuje się tak samo jak standardowe .NET-owe zdarzenia


Definiowanie własnych RoutedEvents wygląda następująco. W pierwszym kroku przy pomocy EventManagera i jego funkcji RegisterRoutedEvent rejestrujemy nasze zdarzenie.

public static RoutedEvent PreviewTrippleClickEvent = EventManager.RegisterRoutedEvent("PreviewTrippleClick", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(MyContentControl));

następnie piszemy wrapper RoutedEventa na zwykłe zdarzenie

public event RoutedEventHandler PreviewTrippleClick
{
add
{
AddHandler(MyContentControl.PreviewTrippleClickEvent, value);
}
remove
{
RemoveHandler(MyContentControl.PreviewTrippleClickEvent, value);
}
}

Funkcja RegisterRoutedEvent przyjmuje cztery parametry:

  • Nazwa naszego zdarzenia - taka sama jak nazwa standardowego eventa opakowującego RoutedEvent
  • Strategia routingu - czyli czy nasze zdarzenie będzie typu Bubble, Tunnel lub Direct

  • Typ handlera - czyli typ delegata/funkcji jaki będzie można podłączyć do zdarzenia, żeby je obsłużyć

  • Typ właściciela - czyli typ klasy do której należy dany event


Jako, że stworzenie nowego RoutedEventa jest dość czasochłonne (w porównaniu ze zwykłym zdarzeniem) warto zassać sobie snippet, który znacząco przyśpiesza tworzenie routed eventów. Snippet taki można znaleźć tutaj RoutedEventSnippet
Mając już zdefiniowany nowy RoutedEvent można go wywołać w następujący sposób:

RaiseEvent(new RoutedEventArgs(MyContentControl.TrippleClickEvent, this));

Funkcja RaiseEvent w parametrze przyjmuje obiekt typu RoutedEventArts. W przypadku gdyby nie był on dla nas wystarczający (potrzebujemy przesłać więcej parametrów itp.), musimy napisać klasę rozszerzającą klasę RoutedEventArts.

Routowanie zdarzenia może zostać zatrzymane, poprzez ustawienie flagi e.Handled na true. Gdzie e jest to obiekt klasy RoutedEventArgs. Przykładowe zatrzymanie routowania eventa może wyglądać w następujący sposób

private void Window_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
}

UWAGA
Można zadeklarować zdarzenie w taki sposób aby wartość flagi e.Handled była pomijana. Jednak można to zrobić jedynie z poziomu kodu. Za pomocą metody AddHandler(RoutedEvent, Delegate, bool) jeżeli ustawimy ostatni parametr na true, wówczas nasza metoda wykona się nawet w przypadku gdy e.Hadnled == true. Metoda ta dostępna jest dla klasy UIElement i wszystkich klas po niej dziedziczących.
Attached events
Attached event działają podobnie jak attached dependency properties. Pozwalają one rozszerzyć kolekcję zdarzeń danej kontrolki o dodatkowe zdarzenia(nawet jeżeli nie mamy dostępu do źródeł danej kontrolki. Przykładowo, istnieje możliwość obsługi eventu click na kontrolce, która tak naprawdę takiego eventu nie posiada. Każdy RoutedEvent może zostać użyty jako attached event.

<Window x:Class="MCTSTrainingChapter1.ThirdMainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ThirdMainWindow" Height="300" Width="300">

<Grid Button.Click="Grid_Click">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Button Grid.Row="0">Top</Button>
<Button Grid.Row="1">Middle</Button>
<Button Grid.Row="2">Bottom</Button>
</Grid>
</Window>

Na gridzie mamy zdefiniowane 3 przyciski, chcielibyśmy aby po naciśnięciu każdego z tych przycisków, wywoływała się odpowiednia funkcja.
Jednym ze sposobów napisania takiej funkcjonalności jest podłączenie eventa Click z każdego przycisku do jednego handlera. Rozwiązanie to jest jak najbardziej poprawne, jednakże w przypadku dużej ilości przycisków może być to czasochłonne. Można zrobić to prościej, przy wykorzystaniu attached events. Jak widzimy w listingu przedstawionym wyżej, do grida "przypięto" event Click z klasy Button. Od teraz
za każdym razem gdy jakikolwiek przycisk będący w obrębie Grida zostanie naciśnięty, zdarzenie to zostanie przechwycone przez handler zdefiniowany w gridzie - Grid_Click

Triggers - Trigery

Trigery jest to mechmizm WPF-a służący reakcji UI na jakieś zdarzenie. Wyróżniamy cztery rodzaje triggerów

  • Property triggers - używane do zareagowanie na zmiany dependency property w danej kontrolce,

  • Data triggers - używane do zareagowanie na zmiany w bindowanych obiektach- można podłączyć się do właściwości z DataContextu jak i z danej kontrolki,

  • MultiDataTriggers - używane do reagowania na zmiany kilku właściwości,

  • MultiTrigger - używane do reagowania na zmiany kilku dependecy property w danej kontrolce,

  • Event triggers - używane do reagowania na jakieś zdarzenie - służą do odpalania animacji



1. Property triggers
Oto przykład wykorzystania Property Triggera. W ResourceDictionary tworzymy styl, który zostanie przypisany do przycisku.

<Style TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Black"></Setter>
</Trigger>
</Style.Triggers>
<Setter Property="Background" Value="Red"/>
</Style>


Widzimy, że dany trigger jest powiązany z właściwością IsMouseOver.W przypadku gdy wartość tej właściwości zostanie ustawiona na true, tło przycisku zostanie zmienione na czarne. Natomiast gdy IsMouseOver powróci do stanu false, tło przycisku wróci do wartości sprzed zmiany.

2. Data triggers
Oto przykład wykorzystania Data Triggera. W ResourceDictionary tworzymy styl, który zostanie przypisany do przycisku.

<Style x:Key="bindingTest" TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Text}" Value="Kolor">
<Setter Property="Background" Value="Beige"/>
</DataTrigger>
</Style.Triggers>
</Style>

W tym przypadku nasz trigger reaguje na zmiany właściwości Text. Jako, że nie ustawiliśmy źródła bindowania, właściwość Text będzie wyszukiwana w DataContext danej kontrolki. W przypadku gdy DataContext nie zostanie ustawiony, właściwość ta będzie wyszukiwana w kontrolce będącej wyżej w hierarchii.W przypadku gdy wartość właściwości Text będzie wynosiła "Kolor" tło kontrolki zostanie zmienione na kolor beżowy.
3. MultiData triggers
Oto przykład wykorzystania MultiDataTriggera. W ResourceDictionary tworzymy styl, który zostanie przypisany do przycisku.

<Style x:Key="MultiDataTrigger" TargetType="{x:Type Button}">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=Text}" Value="Kolor"/>
<Condition Binding="{Binding Path=Title}" Value="Spider-Man"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="Black"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>

Tym razem nasz trigger zareaguje, tylko i wyłącznie gdy wartość właściwości "Text"(znajdującej się w DataContext) będzie równe "Kolor", oraz wartość właściwości "Title" będzie równe "Spider-Man". W chwili gdy warunek ten zostanie spełniony, tło kontrolki zmieni się na kolor czarny.
4. MultiTrigger
Zasada działania MultiTriggera jest analogiczna z zasadą działania MultiDataTriggera. Różnica między ni mi jest taka, że w MultiTriggerze możemy jedynie odnosić się do propertisów danej kontrolki (kontrolki dla której styl tworzymy,oraz kontrolek wchodzących w skład naszej kontrolki). Przykładowy kod MultiTriggera może wyglądać w następujący sposób:

<Style x:Key="MultiTrigger" TargetType="{x:Type Button}">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Background" Value="Black"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Content" Value="MultiTrigger"/>
</MultiTrigger>
</Style.Triggers>
</Style>

Tym razem nasz trigger zareaguje, tylko i wyłącznie gdy tło naszej kontrolki będzie czarne, oraz kursor myszy będzie znajdował się nad przyciskiem. W chwili gdy warunki ten zostaną spełnione, napis na przycisku zmieni się na "MultiTrigger"
5. Event triggers
Event triggery wykraczają poza zakres tej części tutoriala, gdyż są one ściśle związane z animacjami.

WPF - Templates

DataTemplate

W kontrolkach, typu ItemsControl (np. ListBox, ComboBox), które wyświetlają wiele obiektów, można zdefiniować szablon, który definiuje jak ma wyglądać pojedynczy wyświetlany obiekt. Robimy to za pomocą property ItemTemplate, do której przypisujemy obiekt typu DataTemplate.
...Resources>
<DataTemplate x:Key="ItemTemplateKey">
<TextBlock Text="item"/>
</DataTemplate>
</...Resources>

<ListBox ItemTemplate="{StaticResource ItemTemplateKey}" ItemsSource="{Binding Items}"/>

W takim wypadku, każdy Item w listboxie będzie napisem “item”. Dla DataTemplate’a można ustawiać triggery i odwoływać się do elementów wewnątrz za pomocą TargetName.
<DataTemplate.Triggers>
<SomeTrigger...
<Setter Property="BorderBrush" Value="Red" TargetName="border" />
</SomeTrigger>
</DataTemplate.Triggers>


DataTemplateSelector

Jest to klasa, która umożliwia nam wybór jednego z pośród wielu templatów w zależności od wartości lub typu obiektu. Zasadniczo użycie DataTemplateSelector wygląda tak:
public class Selector: DataTemplateSelector
{

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if(item is int && (int)item == 1) return Template1;
if(item is double) return Template2;
return Template3;
}

public DataTemplate Template1 { get; set; }
public DataTemplate Template2 { get; set; }
public DataTemplate Template3 { get; set; }
}

Użycie takiego DataTemplateSelector’a w xaml’u wygląda tak:
<...Resources>
<DataTemplate x:Key="Template1Key">
<TextBlock Text="Pierwszy template"/>
</DateTemplate>
<DataTemplate x:Key="Template2Key">
<TextBlock Text="Drugi template"/>
</DateTemplate>
<DataTemplate x:Key="Template3Key">
<TextBlock Text="Trzeci template"/>
</DateTemplate>
<Selector x:Key="SelectorKey"
Template1="{StaticResource Template1Key}"
Template2="{StaticResource Template2Key}"
Template3="{StaticResource Template3Key}"/>
</...Resources>
<ListBox ItemTemplateSelector="{StaticResource SelectorKey}" ItemsSource="{Binding Items}"/>

W zależności od tego jakiego typu jest dany item i jaką ma wartość zostanie wyświetlony odpowiedni template (odpowiedni napis: “Pierwszy tempalte”, “Drugi template” lub “Trzeci template”).

Na bazie DataTemplateSelector można zrobić bardzo prosty mechanizm MVVM. Załóżmy, że mamy w DataContexcie jakiś obiekt, który ma wystawioną property ViewModel. W widoku mamy
<ContentPresenter Content="{Binding ViewModel}" ContentTemplateSelector="{StaticResource ViewModelTemplateSelector}"/>

Gdzie ViewModelTemplateSelector może wyglądać tak:
public class Selector: DataTemplateSelector
{

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if(item is ViewModel1) return View1;
if(item is ViewModel2) return View2;
return View3;
}

public DataTemplate View1 { get; set; }
public DataTemplate View2 { get; set; }
public DataTemplate View3 { get; set; }
}

DataTemplate możemy zastosować również w przypadku ContentControl lub klasy dziedziczącej po niej. Dobrym przykładem może być Button. Zwykły button wyświetla dane w takiej postaci jak wkleimy do właściwości Content (property content jest domyślna, wszystko co znajduje się pomiędzy jest przypisywane do właściwości Content). Jeżeli napiszemy:
<Button>Napis</Button>

Wówczas uzyskamy przycisk z napisem “napis”. Jeżeli chcemy aby koło naszego napisu zawsze pojawiał się jakiś obrazek wówczas powinniśmy zdefiniować specjalny DataTemplate, który przypisujemy do właściwości ContentTemplate kontrolki typu ContentControl lub pochodnej (np. Button). Nasz DataTemplate wyglądałby mniej więcej tak:
<DataTemplate x:Key="ButtonContentTemplate">
<StackPanel Orientation="Horizontal>
<Image Source="jakiesZrodlo" Widh="10" Height="10"/>
<ContentPresenter Content="{Binding .}"/>
</StackPanel>
</DataTemplate>

Element ContentPresenter jest niezbędny i wskazuje on, w którym miejscu powinna wyświetlić się treść, którą podpinamy do Buttona. Nasz ostateczny wpis wygląda więc tak:
<Button ContentTemplate="{StaticResource ButtonContentTemplate}">Napis</Button>


ControlTemplates

Ok, ale co jeżeli chcemy zmienić cały wygląd buttona, nie tylko treść jaka jest wyświetlana. Wówczas mamy do dyspozycji Property Template. Jest ona typu ControlTemplate i definiujemy ją bezpośrednio w danej kontrolce, w resourcach z odpowiednim kluczem lub w stylu.
<ControlTemplate TargetType="Button" x:Key="ButtonControlTemplate">
<Grid>
<Ellipse Fill="{TemplateBinding Background}"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>

W tym wypadku również umieszczamy element ContentPresenter, aby wskazać w którym miejscu kontrolki powinna się wyświetlić treść.
TemplateBinding oznacza podpięcie domyślnej wartości właściwości Background. TemplateBinding działa podobnie jak RelativeSource.TemplatedParent z tą różnicą, że RelativeSource jest ustalany w trakcie dzialania programu, natomiast TemplateBinding w czasie kompilacji.
Jeżeli chcemy całkowicie zmienić wygląd kontrolki powinniśmy używać właściwości Template, natomiast jeżeli chcemy zachować obecny styl i jedynie zmodyfikować wyświetlane dane wówczas powinniśmy skorzystać z właściwości ContentTemplate.

Style

Style jest to mechanizm wprowadzony w WPF-ie, a następnie w Silverlighcie, dzięki któremu w łatwy sposób można ujednolicić wygląd elementów naszej aplikacji.
Style z reguły definiowane są w zasobach. Mogą to być zarówno zasoby całej aplikacji, danego okna lub nawet wybranej kontrolki.
Przykładowy styl może wyglądać w następujący sposób:

<Style x:Key="przycisk">
<Setter Property="Button.FontSize" Value="22" />
<Setter Property="Button.Background" Value="Orange" />
<Setter Property="Button.Width" Value="60" />
</Style>

Każdy styl jest rozpoznawany dzięki unikalnemu kluczowi - parametr x:Key. Możliwe jest pominięcie klucza, w przypadku gdy ustawiona zostanie właściwść TargetType. Jednakże zdefiniowanie właściwości TargetType spowoduje, że dany styl będzie automatycznie zastosowany do wszystkich kontrolek danego typu. W przypadku gdybyśmy jednak chcieli wyłączyć dany styl dla pojedynczej kontrolki możemy zawsze przypisać mu wartość x:Null
Jak widać styl posiada kolekcję obiektów typu Setter. Najważniejszymi propertisami każdego Settera są:

  • Property - określa nazwę właściwości, którą dany styl będzie modyfikować,

  • Value - określa wartość właściwości określonej w elemencie Property


Dla przykłady - następujący styl:

<Style x:Key="przycisk">
<Setter Property="Button.FontSize" Value="22" />
</Style>

ustawi rozmiar czcionki na buttonie na 22 pkt. Jako, że nie została ustawiona właściwość TargetType dla stylu, musimy podawać pełną nazwę właściwości(czyli nazwę klasy oraz nazwę właściwości).
Oczywiście możemy również ustawiać bardziej skomplikowane właściwości

<Style TargetType{x:Type Button}>
<Setter Property="RenderTransform">
<Setter.Value>
<SkewTransform CenterX=".5" CenterY=".5" AngleX="-10" AngleY="15" />
</Setter.Value>
</Setter>
</Style>

W przypadku gdybyśmy pisali w Silverlighcie właściwość TargetType ustawia się troszeczkę inaczej. Mianowicie zamiast
TargetType{x:Type Button} piszemy TargetType{Button}

Właściwość TargetType określa nam dla jakiego typu obiektów(kontrolek) dany typ jest przeznaczony. Styl z pierwszego przykładu można zapisać następująco:

<Style TargetType={x:Type Button}>
<Setter Property="FontSize" Value="22" />
</Style>

Dzięki użyciu właściwości TargetType, kompilator wie, że dany styl jest przeznaczony dla Buttona, dlatego też możemy w Setterze użyć skróconej nazwy właściwości(FontSize zamiast Button.FontSize). Ponadto dzięki użyciu TargetType, możemy pominąć definiowanie klucza po jakim nasz styl będzie rozpoznawany. Od tej chwili wszystkie buttony będą renderowane z użyciem powyższego stylu (oczywiście jeżeli styl ten będzie w resourcach danej kontrolki).

Dziedziczenie styli

Style mogą być dziedziczone. Oznacza to tyle, że styl pochodny będzie rozszerzał styl bazowy. W przypadku gdy styl bazowy ma zdefiniowany TargetType, styl dziedziczący nie może go zmienić. W celu zdefiniowania dziedziczenia używa się właściwości BasedOn. Przykładowe dziedziczenie styli pokazano poniżej

<Style x:Key="baseStyle">
<Setter Property="FontSize" Value="12" />
<Setter Property="Background" Value="Orange" />
</Style>
<Style x:Key="boldStyle" BasedOn="{StaticResource baseStyle}">
<Setter Property="FontWeight" Value="Bold" />
</Style>

W stylu dziedziczącym można przesłaniać właściwości ze stylu bazowego. W przypadku gdy nie zostaną one przesłonięte, użyte zostaną propertisy ze stylu bazowego.
W celu zaaplikowania stylu do jakiejś kontrolki posługujemy się następującą składnią


myStyle - klucz stylu zdefiniowanego w zasobach.

WPF - Resources

Właściwość Resources jest zdefiniowana w klasie FrameworkElement i jest dostępna we wszystkich klasach jej pochodnych. Właściwość ta zawiera dostępne dla danego widoku zasoby. Property Resources jest typu ResourceDictionary i możemy tam wrzucić dowolny obiekt dostępny w .NET. ResourceDictionary jest to słownik gdzie kluczem jest string lub typ, natomiast wartością może być dowolny obiekt. W xamlu Resources definiujemy w następujący sposób:
<Window.Resources>
<System:String x:Key="hulk">meHulk</System:String>
<Style TargetType="Button"/>
<Style TargetType="{x:Type ListBox}"/>
</Window.Resources>

W przypadku gdy kluczem jest typ możemy go zdefiniować na dwa sposoby ( z użyciem x:Type lub z jego pominięciem) tak jak widać w przykładzie powyżej. W Resources z reguły zamieszczamy wszelkiego rodzaju konwertery, templaty, style lub obiekty potrzebne nam w danym widoku. Można również stworzyć sobie osobny plik xaml typu ResourceDictionary, który nie ma żadnego code behind i wygląda mniej więcej tak:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--Content-->
</ResourceDictionary>

Robi się to w celu uporządkowania kodu i zrobienia go bardziej przejrzystym. W przypadku dużego widoku, który posiada wiele zasobów jest to nieocenione. Jak widać w powyższym przykładzie użyte zostały znaczniki ResourceDictionary, których nie używaliśmy wcześniej, jednak oczywiście poprzedni przykład jest tożsamy z poniższym:
<Window.Resources>
<ResourceDictionary>
<System:String x:Key="hulk">meHulk</System:String>
<Style TargetType="Button"/>
<Style TargetType="{x:Type ListBox}"/>
</ResourceDictionary>
</Window.Resources>

Użycie znaczników ResourceDictionary jest jednak niezbędne tylko wtedy, gdy korzystamy z jakiejś właściwości klasy ResourceDictionary takiej jak np. MergedDictionaries
<Window.Resources>
<ResourceDictionary>
<System:String x:Key="hulk">meHulk</System:String>
</ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"/>
<ResourceDictionary Source="Dictionary2.xaml"/>
</ResourceDictionary.MergedDictionaries>
</Window.Resources>

Właściwość MergedDictionaries dołącza wszystkie oddzielnie zdefiniowane ResourceDictionary do danego widoku. W powyższym przykładzie użytkownik będzie miał swobodny dostęp do wszystkich zasobów zdefiniowanych w słownikach Dictionary1.xaml oraz Dictionary2.xaml. Co więcej z poziomu danej kontrolki mamy dostęp do wszystkich Resources kolejnych obiektów Parent, czyli wszystkich kontrolek znajdujących się wyżej w drzewie wizualnym.
Do obiektów znajdujących się w Resources możemy odwoływać się za pomocą dwóch markup extension: StaticResource i DynamicResource. W przypadku pierwszego mechanizm resourców wczytuje dany obiekt tylko raz, natomist w przypadku DynamicResource obiekt wczytywany jest od nowa przy każdym odwołaniu przez co jeżeli jest zmodyfikowany zmiany będą widoczne. Mówiąc bardziej dokładnie StaticResource jest ustalany w czasie kompilacji, natomiast DynamicResource jest ustalany w czasie działania programu. Zasadniczo StaticResource powinniśmy używać w większości przypadków. Użycie DynamicResource zużywa więcej czasu procesora jednak może przyspieszyć samo uruchamianie się programu. Poza tym może się przydać w przypadku gdy potrzebujemy w czasie działania programu podmienić zasoby (np podmienić skórkę programu).

Warto wspomnieć, o atrybucie Shared typu Boolean. Jeżeli ustawimy go na false, wówczas dla każdego żądania o dany obiekt, będzie zwrócona osobna instancja. Przykładowo:
...Resources>
<Converters:Converter x:Key="Konwerter" x:Shared="False"/>
</...Resources>

Każda kontrolka, która korzysta z “Konwerter” uzyska oddzielną instancję klasy Converter. Domyślnie wszystkie kontrolki współdzielą ze sobą jedną instancję.
Poza tym warto pamiętać, że Resource można “nadpisywać”. Jeżeli w domyślnym stylu ListBox’a użyty jest kolor systemowy HighlightBrushKey wówczas zaznaczenie przez użytkownika dowolnego Item podświetla go na niebiesko. Możemy zmienić ten kolor definiując color o takim samym kluczu:
<ListBox.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
</ListBox.Resources>

Taki wpis zapewni, że nie będziemy mieli podświetlenia domyślnego w naszym ListBox’ie.
Hands on mocking

1. Wprowadzenie

Dokument przybliża idee i metody stosowane przy tworzeniu testów jednostkowych, do symulowania obiektów. Tytuł wynika z popularności pojęcia "Mock", ale nie oznacza, że opisane zagadnienia ograniczają się do tej techniki. Precyzyjne uwagi dotyczące terminologii znajdą się punkcie 2.

Pierwsza część opisuje powody stosowania takich rozwiązań w testach jednostkowych i zyski, które można dzięki nim osiągnąć. W dalszej części przedstawię dwa ogólne podejścia do wykorzystania mocków w procesie wytwarzania oprogramowania.

2. "Doubles" - why do we need them?

a. Terminologia

Zanim przedstawione zostaną motywacje stosowania mockowania i innych technik, konieczne jest usystematyzowanie terminologii, używanej w dalszej części artykulu. Określenie "double" zastosowane w tytule punktu wywodzi się z przemysłu filmowego i oznacza powtórzenie ujęcia. Zostało sformułowane w odniesieniu do oprogramowanie i wykorzystane przez Fowlera w (1).

double - ogólne pojęcie dotyczące każdego sposobu na symulowanie obiektów w testach (mock, stub, fake jest doublem)

dummy - obiekty inicjowane, ale nigdy nie używane. Stosowane do np. do wypełnienia listy argumentów funkcji.

fake - obiekt zaimplementowany, ale tymczasowy - nie nadający się do zastosowania w końcowym produkcie (np. baza danych przechowywana w pamięci). Spotkałem się również z używaniem pojęcia fake w roli takiej jak double, czyli ogólnego określenia. Zazwyczaj znaczenie wynika z kontekstu, proponuję jednak stosować jedną wersję.

stub - funkcja lub pole, które zostało spreparowane do zwracania wybranych, uproszczonych lub trudnych do osiągnięcia wyników. Przesłania on rzeczywistą implementację i pozwala na osiąganie efektów przydatych przy testowaniu (np. "przyspieszanie czasu", zwaracanie specyficznych błędnych wyników, etc.). W dalszych częściach zostaną przedstawione przykłady

mock - najbardziej trafnym będzie określenie - obiekt opakowujący inny obiekt przekazany jako parametr i pozwalający na opisywanie jego zachowania. Za jego pomocą możliwe jest nie tylko "zmuszenie" funkcji do zwrócenia określonego wyniku, ale też określenie jakie metody mockowanej klasy mają zostać wywołane, ile razy i w jakiej kolejności. O ile zatem stub pozwala na testowanie obiektu ze względu na jego stan (state), to mock daje również możliwość przetestowania jego zachowania (behavior).

SUT - System Under Test, czyli obiekt, funkcja, funkcjonalność, która poddawana jest testowaniu

b. Doubles - motywacje

Ostatnie z pojęć przedstawionych w poprzednim punkcie wskazuje na główną ideę testów jednostkowych. Taki test ma weryfikować działanie jednej spójnej części danego systemu, odizolowanej od pozostałych. W każdym teście mamy zatem do czynienia z SUTem oraz otoczeniem, które umożliwia przeprowadzenie testu.

Otoczenie powinno być określone, przewidywalne i możliwe do wygenerowania na żądanie przy przeprowadzeniu testu. Mocki i inne narzędzia tego typu pozwalają na osiągnięcie tego efektu.

c. Przykłady zastosowań wg (2)

Każdy z poniższych przykładów dotyczy sytuacji, gdy testowany SUT, wymaga do poprawnego działania któregoś z wymienionych elementów. Nie należy myilć z sytuacją, gdy testowany jest sam element (nie mockujemy bazy, którą chcemy przetestować). Poniższe punkty nie wyczerpują oczywiście możliwych zastosowań i stanowi raczej podpowiedź przy podejmowaniu decyzji o używaniu Mocków.

i. Symulowanie interakcji z użytkownikiem (kliknięcie myszką, przycisk, etc.)
ii. Web service's
iii. Dostęp do bazy danych
iv. Dostęp do sturktury katalogów, plików i innych elementó związanych z systemem operacyjnym
v. Dostęp do części systemów znajdujących się za fasadą (wzorzec)
vi. Dostęp do klas zdalnych

3. Przykładowa aplikacja

W dalszej części artykułu pojawią się przykłady oparte na grze OneCardMaster. Aplikacja zawiera następujące klasy:

a. OneCardMasterGame (IOneCardMasterGame)
Przykładowe testy będą się odnosić do metod tej klasy, stąd przedstawiony poniżej interfejs. Pozostałe będą w różny sposób symulowane.


namespace OneCardMaster
{
public interface IOneCardMasterGame
{
void AddPlayer(IPlayer player);
void RemovePlayer(IPlayer player);
void SetPlayerLimit(int number);
void PerformGame();
IPlayer GetWinner();
IList GetGameState();
int GetPlayerCount();
}
}

b. Deck (IDeck)
c. Game (IGame)
d. Player (IPlayer)
e. enumy GameColors, GameValues

4. Mocks a TDD

Punkt ma na celu określenie miejsca mocków w strukturze przykładowego testu. Standardowy poprawny test ma strukturę określoną m.in. w (3). Testowanie metody dodawania zawodników, może wyglądać np. w następujący sposób:

/// <summary>
///A test for AddPlayer
///</summary>
[TestMethod()]
public void AddPlayerSimpleTest()
{
// set up
Player player = new Player();
OneCardMasterGame oneCardMasterGame = new OneCardMasterGame();

// execute
oneCardMasterGame.AddPlayer(player);

// verify
Assert.AreEqual(1, oneCardMasterGame.GetPlayerCount());
}

Wyróżnione zostały trzy części. W pierwszej z nich ("set up" lub "arrange") przygotwywane są obiekty, w drugiej ("execute", "act") wykonywana jest testowana operacja, w ostatniej weryfikowane są stany obiektów i w konsekwencji poprawność testu.

Mocki można wykorzystać w pierwszej fazie, do przygotowania środowiska testowego. Test może wyglądać następująco:


///
///A test for AddPlayer
///

[TestMethod()]
public void AddPlayerTest()
{
// set up
IPlayer playerMock = MockRepository.GenerateStrictMock();

// execute
oneCardMasterGame.AddPlayer(playerMock);

// verify
Assert.AreEqual(1, oneCardMasterGame.GetPlayerCount());
}


Jedyna różnica to utworzenie do testów wirtualnego zawodnika, który nie musi posiadać żadnej implementacji. Jak dotychczas różnica nie jest wielka, chociaż można już zauważnyć pierwsze korzyści. Możemy testować metodę AddPlayer, bez jakiejkolwiek implementacji interfejsu IPlayer. Co więcej, sam interfejs nie musi mieć zdefiniowanych żadnych metod.

5. BDD - inny punkt widzenia

Poprzedni punkt zakończyłem stwierdzeniem, że Mocków można używać wobec obiektów które dopiero powstaną. Daje to ciekawe możliwości nie tylko dla testowania, ale też dla projektowania. W TDD testy mają poprzedzać implementację. Rozszerzeniem tej idei jest BDD, czyli Behavior Driven Development.

Załóżmy, że tworzę aplikacjęOneCardMaster. Przyjąłem, że konieczna jest klasa OneCardMasterGame i metody, w tym m.in. PerformGame. Domyślam się, że będę potrzebował w tej grze zawodników (IPlayer) oraz talii (IDeck). Tworzę zatem nastęujący test:



/// <summary>
///A test for PerformGame
///</summary>
[TestMethod()]
public void PerformGameTest()
{
// set up
MockRepository mocks = new MockRepository();
IDeck deck = mocks.StrictMock<IDeck>();
IPlayer player1 = mocks.StrictMock<IPlayer>();
IPlayer player2 = mocks.StrictMock<IPlayer>();

oneCardMasterGame = new OneCardMasterGame(deck);
oneCardMasterGame.AddPlayer(player1);
oneCardMasterGame.AddPlayer(player2);

// execute

oneCardMasterGame.PerformGame();

// verify
mocks.VerifyAll();
}


Wg TDD powinienem teraz stworzyć taką implementację metody PerformGame, żeby test przechodził pomyślnie. Nie jest to jednak możliwe, bo IPlayer ani IDeck nie mają nie zostały jeszcze zaimplementowane. Zastanawiam się zatem jak powinna wyglądać gra i przekładam ten opis na język testu. Gra polega na wybraniu karty z talii i przydzieleniu jej do każdego zawodnika. Tworzę zatem następujący opis:


/// <summary>
///A test for PerformGame
///</summary>
[TestMethod()]
public void PerformGameTest()
{
// set up
MockRepository mocks = new MockRepository();
IDeck deck = mocks.StrictMock<IDeck>();
IPlayer player1 = mocks.StrictMock<IPlayer>();
IPlayer player2 = mocks.StrictMock<IPlayer>();

oneCardMasterGame = new OneCardMasterGame(deck);
oneCardMasterGame.AddPlayer(player1);
oneCardMasterGame.AddPlayer(player2);

// execute

Expect.Call(() => deck.Shuffle());

Expect.Call( deck.GetCard() ).Return( null );
Expect.Call( () => player1.SetCard(null) ).IgnoreArguments();

Expect.Call( deck.GetCard() ).Return( null );
Expect.Call( () => player2.SetCard(null)).IgnoreArguments();

mocks.ReplayAll();
oneCardMasterGame.PerformGame();

// verify
mocks.VerifyAll();
}

Gdy do interfejsów IDeck i IPlayer dodam namiastki metod odpowiednio Shuffle, GetCard dla IDeck i SetCard dla IPlayer test przejdzie pomyślnie. Mogę powtarzać ten proces dla każdej funkcji z interfejsu IOneCardMasterGame i w ten sposób stworzyć pełny obraz interfejsów podrzędnych (IDeck, IPlayer). Następnie powtarzam wszystkie kroki dla niższej warstwy. Np. Za pomocą interfejsu IDeck określam interjes IGame. W ten sposób tworzę aplikację od tworów najogólniejszych do najkonkretniejszych, czyli dość naturalnie. Od pomysłu, do jego realizacji.

Jest to tylko bardzo pobieżny opis tematu BDD a właściwie tylko jednego jego aspektu związanego właśnie z Mockami. Sądzę jednak, że idea jest na tyle ciekawa, że warto poświęcić jej trochę uwagi. Dalsze informacje na temat BDD można znaleźć m.in. w (4) czy innych tekstach tego autora.



6. Rhino Mock, techniki

Dokumentacja Rhino Mocka jest dostępna pod adresem (5), w tym punkcie przedstawię najważniejsze funkcje na przykładowych testach.

a. Podstawowe zastosowanie mocków


/// <summary>
///A test for PerformGame
///</summary>
[TestMethod()]
public void PerformGameTest()
{
// set up
MockRepository mocks = new MockRepository();
IDeck deck = mocks.StrictMock<IDeck>();
IPlayer player1 = mocks.StrictMock<IPlayer>();
IPlayer player2 = mocks.StrictMock<IPlayer>();

oneCardMasterGame = new OneCardMasterGame(deck);
oneCardMasterGame.AddPlayer(player1);
oneCardMasterGame.AddPlayer(player2);

// execute

Expect.Call(() => deck.Shuffle());

Expect.Call( deck.GetCard() ).Return( null );
Expect.Call( () => player1.SetCard(null) ).IgnoreArguments();

Expect.Call( deck.GetCard() ).Return( null );
Expect.Call( () => player2.SetCard(null)).IgnoreArguments();

mocks.ReplayAll();
oneCardMasterGame.PerformGame();

// verify
mocks.VerifyAll();
}


Powyższy przykład posłuży do wyjaśnienia najważniejszych mechanizmów mockowania. Jak już zostało wspomniane, mocki pozwalają na weryfikowanie nie tylko stanu obiektów, ale też ich zachowania. W przykładzie używamy konstrukcji Expect.Call, która pozwala na zdefiniowanie jaka metoda ma zostać wywołana. W tym wypadku zastosowane zostały "restrykcyjne mocki" (StrictMock), więc polecenia muszą być wykonywane w określonej kolejności i ilość.

Po serii Expect.Call dzięki której opisujemy działanie jakiego oczekujemy od testowanej funkcji wykonywane jest polecenie mocks.ReplyAll(), które kończy proces opisywania. Na zakończenie wywoływane jest polecenie mocks.VerifyAll(), które jest odpowidnikiem Assert dla normalnych testów. Sprawdzane jest w tym momencie, czy wszystkie polecenia zostaly wykonane.

Wywołanie Expect.Call(), a następnie IgnoreArguments() pozwala na sprawdzanie wywołania funkcji niezależnie od argumentów.

b. StrictMock, DynamicMock, PartialMock


[TestMethod()]
public void TestExampleWithStrictMock()
{
MockRepository mocks = new MockRepository();
IPlayer demo = mocks.StrictMock<IPlayer>();
mocks.ReplayAll();
demo.Compare(null);
mocks.VerifyAll();//will never get here
}

Zastosowanie "restrykcyjnego mocka" powoduje, że wykonane mają być dokładnie wskazane funkcje i tylko one. W przykładzie próbujemy wykonać funkcję, która nie została przewidziana przed wywołaniem mocks.ReplayAll(), stąd wyjątek i test nie przechodzi.

Gdyby zamiast mocks.StrictMock() użyć mocks.DynamicMock() test zakończyłby się sukcesem. Dynamiczne mocki weryfikują czy określone metody zostały wykonane, ale nie wykluczają użycia innych metod.

PartialMocks pozwala na mockowanie części klasy. Np. gdy chcemy testować klasę abstrakcyjną, której część metod jest zaimplementowana, możliwe jest mockowanie tylko tych metod które nie mają implementacji.

c. Stub


/// <summary>
///A test for GetWinner
///</summary>
[TestMethod()]
public void GetWinnerTest()
{
// set up

IDeck deck = MockRepository.GenerateStrictMock<IDeck>();
IPlayer player1 = MockRepository.GenerateStub<IPlayer>();
IPlayer player2 = MockRepository.GenerateStub<IPlayer>();
IPlayer player3 = MockRepository.GenerateStub<IPlayer>();

oneCardMasterGame = new OneCardMasterGame(deck);
oneCardMasterGame.AddPlayer(player1);
oneCardMasterGame.AddPlayer(player2);
oneCardMasterGame.AddPlayer(player3);

// execute

player1.Stub(val => player1.Compare(val)).Return(true);
oneCardMasterGame.PerformGame();
IPlayer winner = oneCardMasterGame.GetWinner();

// verify

Assert.AreSame(player1, winner);

}


W powyższym przykładzie testowana jest funkcja wskazująca zwycięscę gry, czyli zawodnika, który wylosował najwyższą kartę. Zawodnicy mogą być porównywani za pomocą metody Compare. Zwycięscą powinien być zawodnik, który w każdym porównaniu jest lepszy od przeciwnika. W tym scenariuszu, nie ma potrzeby sprwadzania kolejności wykonywanych funkcji. Tworzymy fikcyjnego "kandydata na zwycięscę" i określamy, że wygrywa on przy porównaniu z każdym. Zatem funkcja GetWinner powinna wskazać player1 jako wygranego gry.

d. Ograniczenia funkcji

W poprzednich przykładach została użyta metod IgnoreArguments(). Możliwe jest też dodawanie ograniczeń do argumentów, bez dokładnego ich specyfikowania, za pomocą metody Constraint().

e. Powtarzanie wywołań
Za pomocą LastCall.Repeat.Once() możliwe jest ponowne wywołanie ostatniego zapytania. LastCall.Repeat zawiera również inne metody pozwalające na automatyczne używanie pewnych funkcji.


Dalsze przykłady można znaleźć w (5) i (6). Sądzę też, że w miarę rozwoju naszej aplikacji, każdy spotka się z sytuacjami wymagającymi bardziej złożonego testu, a zatem również specyficznego zastosowania Mocków. Warto takie przykłady zbierać i uzupełnić poniższy dokument.

7. Podsumowanie

Dokument przedtawia tematykę mockowania dość pobieżnie, ale sądzę że wystarczająco, aby z jego pomocą zacząć to narzędzie stosować. Pojawia się jednak pytanie - gdzie i kiedy mocków używać? Fowler w dokumencie (1) zestawia dwa podejścia które nazywa "calssical TDD" i "mockist TDD". Pierwsze oznacza korzystanie mocków, tylko tam, gdzie stosowanie normalnych implementacji byłoby problematyczne, niewygodne, kosztowne. Czyli najczęściej w przypadkach wymienionych w punkcie 3. Praktycy "mockist TDD" promują stosowanie mocków wszędzie poza kluczową, testowaną funkcjonalnością (można użyć kolokwialnego określenia, poza SUTem). Fowler wskazuje jednak, że o ile podobny pomysł wygląda wspaniale jako artykuł na konferencję, to w przypadku tworzenia biznesowego kodu jest już zdecydowanie trudniejszy i żmudniejszy do zastosowania. Uważam, że przy wyborze obiektów do mockowania powinniśmy się kierować przede wszystkim zdrowym rozsądkiem, zasadmi czystości i czytelności kodu. Podobnie w przypadku decydowaniu pomiędzy Mockami a Stubami. Jeśli nie jest nam potrzebne weryfikowanie kolejności wykonywanych poleceń, Stub w zupełniości wystarczy. Przy praktyce mockowania i wątpliwościach dotyczących składni polecam przede wszystkim dokumenty (5) i (6). Więcej o zaletach "mockist TDD" można znaleźć w (7).

Bibliography
1. Fowler, Martin. Martin Fowler's software development blog. [Online] martinfowler.com/articles/mocksArentStubs.html.
2. Miller, Jeremy. Code better. Why and When to Use Mock Objects. [Online] http://codebetter.com/jeremymiller/2005/12/20/why-and-when-to-use-mock-objects/.
3. Weibel, Patrick. assumption of advancement. How to structure code in an unit test. [Online] http://blog.eweibel.net/?p=200.
4. North, Dan. Introducing BDD. [Online] http://dannorth.net/introducing-bdd/.
5. Rahien, Ayende. [Online] http://ayende.com/wiki/Rhino+Mocks+Documentation.ashx.
6. Aniserowicz, Maciej. Maciej Aniserowicz o programowaniu. [Online] http://www.maciejaniserowicz.com/post/2009/09/29/Spis-tresci-Cykl-o-mock-objects-i-Rhino-Mocks.aspx.
7. Steve Freema, Tim Mackinnon, Nat Pryce, Joe Walnes. Mock Roles, Not Objects (OOPSLA 2004). [Online] 2004. http://www.jmock.org/oopsla2004.pdf.

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management