Fake, Dummy, Stub, Mock, Test Double–krótkie wyjaśnienie terminologii używanej w testach jednostkowych

Czytając różne książki lub artykuły można się zgubić w terminologii. Szczególnie Mock oraz Stub często są używane zamiennie a ich różnica jest jednak znacząca. Nie tłumaczyłem na język polski powyższych terminów aby nie wprowadzać jeszcze większego zamieszaniaUśmiech

Zacznijmy od terminu najbardziej ogólnego – Double. Nazwa określa dowolny obiekt  naśladujący realną klasę. Zatem obiektem double może być Fake, Dummy, Stub lub Mock. Innymi słowy double można podzielić na wspomniane 4 obiekty. Termin stanowi po prostu ogólne określenie dowolnego obiektu, używanego podczas testów jednostkowych w celu izolacji. Nazwa wzięła się z filmów w których często pracę aktora w niebezpiecznych scenach zastępują dublerzy (kaskaderzy, stunt double).

Pierwszym typem Double jest Dummy, który stanowi najbardziej prymitywny sposób izolacji. Dummy używamy gdy nie ma dla nas znaczenia co dana klasa zrobi a po prostu zaprojektowana architektura wymagania przekazania jakiegoś obiektu. Wtedy tworzymy Dummy (atrapę), która po prostu nic nie robi. Dzięki temu będziemy mogli przetestować pozostałą funkcjonalność, izolując całkowicie  klasę, której nie chcemy weryfikować. Załóżmy, że mamy klasę Order:

interface IMessageBoxService
{
    Result Show(string message);
}
class Order
{
    private IMessageBoxService _messageBoxService;
    private IStockRepository _stockRepository;    
    public Product(IMessageBoxService msgService, IStockRepository stockRepository)
    {
        _messageBoxService = msgService;
        _stockRepository = stockRepository;
    }
    
    public bool Submit()
    {    
        msgService.Show("Proszę czekać - sprawdzanie zamówienia");
        
        int remainingQuantity=_stockRepository.GetQuantity(IdProduct);
        if( Quantity > remainingQuantity)
            return false;        
        
        // ... - jakas inna logika    
        
        msgService.Show("Gotowe!");
        return true;
    }
    // pozostala czesc logiki
}

W skrócie, za pomocą DI wstrzykujemy implementacje MessageBoxService oraz pewnego repozytorium służącego m.in. do sprawdzenia liczby sztuk towaru w magazynie. Następnie metoda Submit wyświetla dwa komunikaty oraz sprawdza czy żądana ilość towaru może zostać wydana. Oczywiście zadanie postawione to przetestowanie metody Submit. Załóżmy, że nie interesuje nas kompletnie to co robi MessageBoxService ponieważ jest to “tylko” warstwa prezentacji. Możemy użyć w tej sytuacji obiektu Dummy:

class DummyMsgBoxService: IMessageBoxService
{
    public Result Show(string message)
    {
        return Result.OK;
    }
}

Powyższa implementacja jest całkowitą atrapą – metoda nic tak naprawdę nie robi.

Kolejną, bardziej zaawansowaną konstrukcją jest obiekt Fake. W przeciwieństwie do Dummy, zawiera już pewną logikę jednak nie taką samą jak prawdziwy obiekt. W przypadku repozytorium, może używać zwykłą kolekcję danych (in-memory), zamiast odniesień do prawdziwej bazy danych.

Pozostały najbardziej mylone obiekty Stub i Mock. Stub używamy w testach jednostkowych wykorzystujących weryfikację stanu obiektów. Mock  z kolei używamy w testach opartych na badaniu zachowania (behaviour-based testing). Zacznijmy od krótkiego opisu testów opartych na stanie obiektów ponieważ są one najbardziej popularne. Stwórzmy więc Stub dla repozytorium:

class StubStockRepository: IStockRepository
{
    public int GetQuantity(int idProduct)
    {
        return 5;
    }
}

Jak widać, Stub zawsze zwraca z góry zdefiniowane wyniki. Następnie test dla Submit będzie wyglądać następująco:

public void SubmitTest()
{
    Order order=new Order(new StubStockRepository()){Quantity=10};
    bool result = order.Submit();
    
    Assert.IsFalse(result);
}

Test sprowadzana się zawsze do weryfikacji stanu obiektu lub zwróconej wartości. W testach z wykorzystaniem Stub wywołujemy metodę a następnie sprawdzamy właściwości klasy aby upewnić się, że wywołanie metody zmieniło odpowiednie pola w klasie. Z kolei sam Stub zwraca predefiniowaną przez nas, przewidywalną wartość.

Mock  opiera się na zachowaniach. Najpierw definiujemy nasze oczekiwania w formie jaka metoda powinna być wykonana, następnie wywołujemy testowaną metodę i sprawdzamy czy nasze oczekiwania zostały spełnione. Oczekiwania w przypadku metody Submit są następujące (przy założeniu, że w magazynie jest żądania ilość towaru):

  1. Metoda Show (IMessageBoxService) powinna zostać wywołana dwa razy.
  2. Metoda GetQuantity powinna zostać wywołana dokładnie raz. Ponadto w założeniu można zdefiniować przekazany parametr do GetQuantity – musi to być identyfikator produktu.

Z wykorzystaniem framework’u Moq, można test wykonać następująco:

[Test]
public void SubmitTest()
{
    var msgServiceMock = new Moq.Mock<IMessageBoxService>();
    var stockRepositoryMock = new Moq.Mock<IStockRepository>();
    
    Order order = new Order(msgServiceMock,stockRepositoryMock){Quantity = 3};
    order.Submit();

    // weryfikacja zdefiniowanych zachowań
    msgServiceMock.Verify(r => r.GetQuantity(Moq.It.IsAny()),Moq.Times.Once());
    stockRepositoryMock.Verify(m => m.Show(Moq.It.IsAny()),Moq.Times.Exactly(2));
    
}

Zamiast sprawdzania wyniku zwróconego przez Submit, weryfikujemy które metody zostały wykonane.  Możemy oczywiście uszczególnić i określić dokładnie jakie parametry powinny być przekazane, które metody nie powinny zostać wywołane oraz jakie operacje są dozwolone na właściwościach.

Więcej o testach typu behaviour, mam nadzieję, że napiszę wkrótce…

MemoryCache–buforowanie w .NET 4.0

W .NET 4.0 dodano (mało chyba popularną) klasę MemoryCache służącą do buforowania danych. Wcześniej programiści znali podobną klasę w ASP.NET. Od wersji 4.0 została ona wydzielona i można z niej teraz korzystać we wszystkich typach aplikacji – także WPF i WinForms.  Za pomocą MemoryCache można ustawić czas  ważności danych w buforze za pomocą czasu absolutnego,  priorytetu, monitorów, które unieważniają dane np. na podstawie zmiany pliku na dysku lub tzw. SlidingExpiration – czasu który upłynął od ostatniego odpytania bufora.

Same API jest bardzo proste. Na przykład aby dodać element do bufora wystarczy:

CacheItemPolicy cachePolicy = new CacheItemPolicy();
cachePolicy.AbsoluteExpiration = new DateTimeOffset(new DateTime(2012, 11, 1));
MemoryCache.Default.Add("klucz", "dowolna wartosc", cachePolicy);

Z kolei aby odczytać z bufora wartość:

string value = MemoryCache.Default.Get("klucz").ToString();

Kilka wyjaśnień:

  1. AbsoluteExpiration – oznacza kiedy dana wartość zostanie skasowana z bufora (absolutny czas ważności).
  2. MemoryCache.Default zwraca domyślny bufor – zwykłe aplikacja potrzebuje tylko jednego MemoeryCache i nie ma potrzeby tworzenia własnych obiektów MemoeryCache.
  3. Gdy bufor straci ważność metoda Get zwraca NULL – stąd powyższy kod jest niebezpieczny ponieważ może wywołać wyjątek null reference  przez ToString().

Oprócz absolutnego czasu ważności możemy zdefiniować wspomniany SlidingException:

CacheItemPolicy cachePolicy = new CacheItemPolicy();
cachePolicy.SlidingExpiration = new TimeSpan(0, 1, 0);

Powyższy kod oznacza, że jeśli wpis w buforze nie będzie wykorzystany (poprzez metodę Get) ani razu w ciągu minuty to zostanie unieważniony.

Do dyspozycji mamy również kilka różnych monitorów:

List<string> paths=new List<string>();
paths.Add("c:\\1.txt");
HostFileChangeMonitor fileChangeMonitor=new HostFileChangeMonitor(paths);
cachePolicy.ChangeMonitors.Add(fileChangeMonitor);

HostFileChangeMonitor sprawdza  czy przekazane przez parametr paths, pliki lub foldery nie zostały zmienione. Monitorowane są m.in. nazwy plików (lub katalogów), rozmiar, ACL czy pliki w danym katalogu. Ponadto do despocji mamy jeszcze następujący monitory:

  1. CacheEntryChangeMonitor – unieważnia wpis w przypadku gdy jakiś inny wpis zostały zmieniony.
  2. SqlChangeMonitor – monitorowanie bazy SQL Server.

Do dyspozycji jest również priorytet, który aktualnie przyjmuje dwie wartości (wydaje mi się, że nazwy mówią same za siebie):

cachePolicy.Priority = CacheItemPriority.Default;
cachePolicy.Priority = CacheItemPriority.NotRemovable;

Oprócz poszczególnych elementów w buforze, można również konfigurować bufor jako całość. Domyślny bufor (zwrócony poprzez właściwość Default) można wyłącznie skonfigurować za pomocą XML (np. w App.config dla aplikacji desktop):

<configuration>
  <system.runtime.caching>
    <memoryCache>
      <namedCaches>
          <add name="default" 
               cacheMemoryLimitMegabytes="0" 
               physicalMemoryPercentage="0"
               pollingInterval="00:02:00" />
      </namedCaches>
    </memoryCache>
  </system.runtime.caching>
</configuration>

Własne bufory można konfigurować zarówno z poziomu pliku XML jak i konstruktora.

C# 4.0–parametry opcjonalne oraz named arguments

C# 4.0 dostarcza kilka nowych możliwości jeśli chodzi o parametry i ich przekazywanie. Pierwszym rozszerzeniem jest możliwość zdefiniowania opcjonalnych argumentów (dlaczego musieliśmy tak długo na to czekaćUśmiech?):

private void ShowText(string text="Hello world")
{
  MessageBox.Show(text);
}

Teraz możemy wywołać metodę ShowText dostarczając własny argument lub pomijać go i wykorzystując wartość domyślną (“Hello World”):

ShowText("Custom Text");
ShowText(); // również poprawne

Named arguments umożliwiają przekazanie parametrów do wywołania funkcji za pomocą ich nazwy (przekazywane wartości poprzedzamy nazwą parametru oraz dwukropkiem). Wyobraźmy sobie następującą funkcję:

private void ShowText(string text1, string text2 = "Hello world 2", string text3="Hello world3")
{
  MessageBox.Show(string.Format("text1:{0}", text1));
  MessageBox.Show(string.Format("text2:{0}", text2));
  MessageBox.Show(string.Format("text3:{0}", text3));
}

Jak przekazać funkcji wymagany parametr text1 oraz opcjonalny text3 a jednocześnie wykorzystać domyślną wartość text2? Poniższa próba zakończy się fiaskiem:

ShowText("text1", "text3");

text1 zostanie ustawiony na “text1” z kolei text2 na “text3” – a text3 posiadać będzie wartość domyślną czego nie chcemy. Z pomocą przychodzą wspomniane named arguments:

ShowText("text1", text3:"text3");

Zamiast przekazywać argumenty bazując na ich kolejności, wykorzystujemy po prostu ich nazwę. Ponadto jeśli nasza funkcja przyjmuje dużą liczbę parametrów (np. 5) warto skorzystać z named arguments aby za pomocą nazw wyjaśnić co te argumenty robią. Np. następująca funkcja na pierwszy rzut oka może niewiele mówić:

UpdateZoom(1,5,4);

Z kolei za pomocą named arguments możemy stworzyć samo-komentujący się kod:

UpdateZoom(minZoom:1,maxZoom:5,currentZoom:4);

Jak widać drugie wywołanie jest bardziej czytelne i więcej mówi o tym co funkcja wewnątrz robi.

Wróćmy na chwilę jeszcze do domyślnych argumentów. Opcjonalne wartości  zostały  wprowadzone również z myślą o COM. Wcześniej wdrożenie bibliotek COM do c# było czasami bardzo niewygodne. Klasy z COM mają bardzo dużo metod z wartościami domyślnymi, które zwykle nie są przekazywane przez programistę. Przykład (źródło MSDN):

Document d = new Document();
object filename = "Foo.docx";
object missing = Type.Missing;

d.SaveAs(ref filename, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing);

W c# 4.0 kod uprości się do:

Document d = new Document();

d.SaveAs(FileName: "Foo.docx");

W bibliotekach .NET często widzimy wiele podobnych sygnatur różniących się tylko liczbą parametrów. Programiści w ten sposób (przeładowywaniem metod) radzili sobie z brakiem opcjonalnych argumentów. Od 4.0 możemy znaczącą skrócić kod, dostarczając wyłącznie jedną metodę ale za to z opcjonalnymi wartościami. Czyli zamiast:

private void ShowText(string text1)
{
  ShowText(text1, "Hello world 2", "Hello world 3");
}
private void ShowText(string text1,string text2)
{
  ShowText(text1, text2, "Hello world 3");
}
private void ShowText(string text1, string text2, string text3)
{
  MessageBox.Show(string.Format("text1:{0}", text1));
  MessageBox.Show(string.Format("text2:{0}", text2));
  MessageBox.Show(string.Format("text3:{0}", text3));
}

Wystarczy:

private void ShowText(string text1, string text2 = "Hello world 2", string text3="Hello world3")
{
  MessageBox.Show(string.Format("text1:{0}", text1));
  MessageBox.Show(string.Format("text2:{0}", text2));
  MessageBox.Show(string.Format("text3:{0}", text3));
}

Jak serializować interfejsy w C#?

XMLSerializer jest bardzo wygodnym sposobem zapisu klas w formie pliku XML. Rozważmy następujący przykład:

public interface IData
{
    // jakies dekleracje
    string Text{get;set;}
}
public class Data: IData
{
    public string Text{get{...}set{...}}
}
public class ClassA
{
    public IData Data{get;set;}
}

Niestety po próbie serializacji dostaniemy wyjątek mówiący, że nie można zapisywać interfejsu. Jaka jest odpowiedź na pytanie zadanie w poście? NIE da się!

W poście mam zamiar pokazać wyłącznie pewne sposoby obejścia tego – dalekie od ideału. Po pierwsze możemy stworzyć klasę abstrakcyjną zamiast interfejsu:

public abstract class DataBase
{
    // jakies dekleracje
    public abstract string Text{get;set;}
}
public class Data: DataBase
{
    public override string Text{get{...}set{...}}
}
public class ClassA
{
    public DataBase Data{get;set;}
}

Oczywiście ma to swoje wady – możemy dziedziczyć tylko po jednej klasie (klasa abstrakcyjna jest typem) itp. Jednak dzięki temu możemy obejść wymogi XMLSerialzer i wciąż mieć bazowy “interfejs”. Jeśli jednak w innych miejscach kodu wciąż chcemy odwoływać się za pomocą interfejsu możemy dodać do naszej hierarchii również interfejs:

public interface IBase
{
    string Text[get;set;}
}
public abstract class DataBase:IBase
{
    // jakies dekleracje
    public abstract string Text{get;set;}
}
public class Data: DataBase
{
    public override string Text{get{...}set{...}}
}
public class ClassA
{
    public DataBase Data{get;set;}
}

Niestety jest to jeden z tych postów, który nie dostarcza odpowiedzi na postawione pytanie Uśmiech. Po prostu jej nie ma… Jeśli faktycznie jest to niezbędne w architekturze pozostaje tylko rozszerzenie lub napisanie od nowa własnego serializatora.

Nieobsłużone wyjątki w WPF

Domyślnie WPF wyświetla okno dialogowe nieobsłużonego błędu a następnie po przyciśnięciu przycisku aplikacja zostaje zamknięta. W poście zajmiemy jednak się przeładowaniem tego zachowania poprzez np. zapis błędu do pliku. W WPF możemy wykorzystać zdarzenie DispatcherUnhandledException(plik App.xaml.cs), która wywoływana jest dla nieobsłużonych wyjątków. Jeśli zatem w kodzie znajdzie się wywołanie throw bez klauzuli catch, zdarzenie DispatcherUnhandledException zostanie uruchomione. Przykład, plik app.xaml.cs

public partial class App : Application
{
    private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
    {
        // zapis do logów, wyświetlanie własnego okna błędów itp.
        e.Handled = true;
    }
}

app.xaml:

<Application x:Class="WpfApplication2.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             DispatcherUnhandledException="App_DispatcherUnhandledException"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

Ustawienie wartości e.Handled na true powoduje, że wyjątek zostanie potraktowany jako obsłużony. Jest to więc doskonałe miejsce na wyświetlenie wszelkich customowych okien dialogowych czy umieszczenie po prostu logger’a. W pliku XAML podpinamy tylko zdarzenie (DispatcherUnhandledException ).

Niestety powyższy kod będzie działał wyłącznie w sytuacji gdy to główny wątek wyrzuca wyjątek. Dla wszelkich innych wątków (np. w BackgroundWorker, Thread itp), metoda NIE zostanie wywołana. Jeśli zatem chcemy dodać obsługę wyjątków wyrzuconych w zewnętrznych wątkach, musimy:

  1. obsłużyć wyjątek w zewnętrznym wątku (catch),
  2. przekazać wyjątek poza wątek (czyli do głównego wątku)
  3. rethrow – wyrzucić ponownie wyjątek – w tym momencie zostanie już uruchomiona metoda App_DisptacherUnhandledException.

ContextMenu, DataContext oraz problemy z binding

Jakiś czas temu napotkałem na problem podczas próby przypisania własnego kontekstu kontrolce ContextMenu. Zacznijmy może od razu od przykładu XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        Title="Sample" Height="191" Width="337">    
    <Grid x:Name="LayoutRoot" Background="DarkBlue">
        <Grid.ContextMenu>
            <ContextMenu DataContext="{Binding SimpleViewModel}">
                <ContextMenu.Items>
                    <MenuItem Header="{Binding Text}"/>
                </ContextMenu.Items>
            </ContextMenu>
        </Grid.ContextMenu>            
    </Grid>
</Window>

Prosty widok  – zwykły Grid z kontekstowym menu. DataContext ustawiam na SimpleVIewModel a następnie binduje właściwość Text. Code-behind wygląda następująco:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        SimpleViewModel = new WpfApplication1.SimpleViewModel();
        DataContext = this;
        InitializeComponent();
    }
    public SimpleViewModel SimpleViewModel { get; private set; }
}
public class SimpleViewModel
{
    public string Text { get { return "Hello"; } }
}

Kod chyba nie wymaga komentarza. Po uruchomieniu aplikacji spodziewałem się, że w menu zostanie wyświetlony tekst “Hello”. Niestety ku mojemu zdziwieniu żaden tekst nie pojawił się. Po krótkim research’u  okazało się, że ContextMenu nie jest w WPF składową drzewa elementów (visual tree) i wszelkie bindingi (nawet te z ElementName) nie zadziałają. DataContext jest ustawiany na początku na kontekst rodzica i nie można go w podany sposób przeładować.

Istnieje na szczęście pewne, eleganckie obejście problemu. Otóż ContextMenu posiada właściwość PlacementTarget, które wskazuje na obiekt na którym znajduje się aktualnie menu (w tym przypadku Grid). Za pomocą PlacementTarget można zatem dostać się do DataContext rodzica a następnie do SimpleViewModel! Oto przykład:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        Title="Sample" Height="191" Width="337">    
    <Grid x:Name="LayoutRoot" Background="DarkBlue">
        <Grid.ContextMenu>
            <ContextMenu DataContext="{Binding PlacementTarget.DataContext.SimpleViewModel,RelativeSource={RelativeSource Self}}">
                <ContextMenu.Items>
                    <MenuItem Header="{Binding Text}"/>
                </ContextMenu.Items>
            </ContextMenu>
        </Grid.ContextMenu>            
    </Grid>
</Window>

RelativeSelf ustawia jako kontekst obiekt kontrolki – uzyskujemy więc dostęp do wszelkich publicznych właściwości kontrolki (a więc również do PlacementTarget). Następnie poprzez DataContext dostajemy się do naszego SimpleViewModel. Po uruchomieniu aplikacji, przekonamy się, że teraz wiązanie danych zostało przeprowadzone poprawnie.

Kowariancja i kontrawariancja a C# 4.0 oraz typy generyczne

Zanim przejdę do wyjaśnienia kilku usprawnień wprowadzonych w C# 4.0 spróbujmy zdefiniować pojęcia kowariancji oraz kontrawariancji. Te skomplikowane pojęcia odnoszą się po prostu do typów konwersji.

Kowariancja to określenie typu konwersji  z bardziej specyficznego do bardziej ogólnego(klasy). Kontrawariancja to oczywiście przeciwieństwo (bazowa klasa do pochodnej). Jako przykład kowariancji w c# można pokazać np.:

object text = "Hello World!";

Deklaracja text jest kowariancją ponieważ możemy przypisać do zmiennej tylko takie same lub bardziej specyficzne typy (string jest subtypem object). Podobnie tablice są przykładem kowariancji –jesteśmy w stanie użyć następującej deklaracji:

object[] strings = new string[1];

Załóżmy, że mamy 3 klasy (classA, classB, classC):

class classA{}
class classB:classA
class classC:classB

Oraz następującą metodę:

public classB Method(classB parameter)
{
    // logika
}

Typ zwracany jest zawsze kowariancją. Możemy zatem wykorzystać takie konstrukcję:

classB b = Method(par);
classA a = Methhod(par);

Kowariancja czyli konwersja z bardziej precyzyjnego (classA) do bardziej ogólnego(classB). Z kolei argumenty (parametry) są zawsze kontrawariancją:

Method(new classB());
Method(new classC());

Nie możemy przekazać jako parametr wartości bardziej ogólnej (classA). Mamy sytuacje odwrotną i jedynymi dozwolonymi wartościami są klasy bardziej specyficzne.

Typy generyczne w c# 3.0 były  invariant (neutralne, niezmienne). Oznacza to, że nie może nastąpić żadna konwersja. Jeśli metoda zwraca IEnumarable<classB> wyłącznie takowy typ może zostać użyty:

private IEnumerable<classB> Method()
{
    return null;
}
//
IEnumerable<classA> listA = Method(); // Błąd - typy generyczne w c# 3.0 nie są kowariancją
IEnumerable<classB> listB = Method(); // OK - invariant
IEnumerable<classC> listC = Method(); // Błąd - typy generyczne w c# 3.0 nie są kontrawariancją

W c# 4.0 typy generyczne mogą jednak zostać zdefiniowane jako kowariancja. Interfejs IEnumarable jest przykładem kontrawariancji i jeśli utworzymy nowy projekt w VS 2010 w wersji c# 4.0 poniższy kod będzie całkowicie poprawny:

IEnumerable<classA> listA = Method(); // W wersji C# 4.0 poprawne!
IEnumerable<classB> listB = Method();    

Kowariancję w typach generycznych w c# 4.0 oznacza się słowem kluczowym out. Dlatego też, IEnumarable jest zdefiniowany następująco:

public interface IEnumerable<out T> // zamiast public interface IEnumerable<T> 

Warto również wspomnieć, że tylko interfejsy oraz delegates mogą używać słowa kluczowego out.

To nie wszystko jednak… W c# 4.0 można również zdefiniować kontrawariancję za pomocą słowa in:

interface ICustomGenericType<in T>
{
}

Teraz poprawna stanie się następująca konstrukcja:

private ICustomGenericType<classB> Method()
{
    return null;
}

ICustomGenericType<classB> listB = Method();
ICustomGenericType<classC> listC = Method();

Może komuś to się przyda a jeśli nie to przynajmniej będzie wiedział dlaczego pewien kod kompiluje się w c# 4.0 a w c# 3.0 już nieUśmiech.

Binding zdarzeń w WPF, MVVM cz.2

Kilka miesięcy temu pisałem o attached behaviour jako sposobie na wiązanie zdarzeń do komend. Sposób całkowicie poprawny i wygodny w użyciu. Dzisiaj jednak chciałbym przedstawić nieco prostsze rozwiązanie z wykorzystaniem bibliotek z Expression Blend SDK.

Jeśli jeszcze nie posiadacie SDK możecie je ściągnąć ze strony Microsoft’u. Do projektu dołączamy  bibliotekę System.Windows.Interactivity (SDK). Następnie w pliku XAML spróbujmy powiązać zdarzenie MouseMove z komendą ShowMsgCmd:

<Button>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseMove">
            <i:InvokeCommandAction Command="{Binding ShowMsgCmd}"/>                    
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

Namespace “i” powinien być skojarzony z biblioteką System.Windows.Interactivity:

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

Jak widać Expression Blend dostarcza pewnie rozszerzenie dla wyzwalaczy. Możemy w ten sposób wykonać własny kod w momencie odpalenia wyzwalacza (który jest uruchamiany w tym przypadku na podstawie zdarzenia). W EventTrigger definiujemy własne akcje – klasy, które dziedziczą po TriggerAction. InvokeCommandAction dziedziczy po TriggerAction i wywołuje wskazaną komendę. Możemy nawet podać jakiś argument do komendy:

<Button>
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseMove">
                    <i:InvokeCommandAction Command="{Binding ShowMsgCmd}" CommandParameter="argument"/>                    
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>

Niestety często potrzebować będziemy argumentów zdarzenia (EventArgs). Aktualna implementacja InvokeCommandAction nie pozwala na to. Aby móc w obsłudze (Execute) korzystać z EventArgs musimy zaimplementować własny TriggerAction:

public sealed class CustomCommandInvoker : TriggerAction<DependencyObject>
    {                    
        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
            "Command", typeof(ICommand), typeof(CustomCommandInvoker), null);
    
        public ICommand Command
        {
            get
            {
                return (ICommand)this.GetValue(CommandProperty);
            }
            set
            {
                this.SetValue(CommandProperty, value);
            }
        }                
        protected override void Invoke(object parameter)
        {

            if (this.AssociatedObject != null)
            {
                ICommand command = Command;
                if ((command != null) && command.CanExecute(parameter))
                {
                    command.Execute(parameter);
                }
            }
        }
    }

Jak widać, implementacja nie jest skomplikowana. Zastosowanie wygląda bardzo podobnie:

<Button>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseMove">
            <customInvokers:CustomCommandInvoker Command="{Binding ShowMsgCmd}"/>                    
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

Następnie w obsłudze komendy możemy wykorzystać EventArgs dostarczane przez zdarzenia:

private void ShowMsg(object args)
{
    MouseEventArgs mouseArgs = (MouseEventArgs)args;
    MessageBox.Show(string.Format("Hello world! {0}:{1}", mouseArgs.GetPosition(this).X, mouseArgs.GetPosition(this).Y));
}

Na zakończenie warto dodać, że biblioteka z ExpressionBlend nie jest “ciężka” – zawiera dosłownie kilka klas. Dodając ją do projektu nie musimy się martwić o to, że dodamy wiele niepotrzebnych innych klas.

Message Boxes w MVVM

Wzorzec Model-View-ViewModel jest najczęściej wykorzystywany przez programistów WPF. Związane jest to z mechanizmem wiązań, który znacząca ułatwia wdrożenie MVVM do projektu. ViewModel stanowi wyeksponowany model przeznaczony do bindingu, z kolei widok to zwykły, pasywny plik XAML. Ważną cechą ViewModel jest brak powiązań z interfejsem użytkownika. Najlepiej aby projekt zawierający ViewModel nie posiadał żadnych referencji do bibliotek warstwy prezentacji.

Częstym zadawanym pytaniem jest: Jak wywoływać standardowe okna komunikatów  (MessageBox)? Aby poprawnie zaimplementować VM, nie powinno się wywoływać komunikatów bezpośrednio VM. Umieszczenie wywołań w Code-Behind również nie jest dobrym podejściem (często messagebox zależą od logiki biznesowej oraz nie powinniśmy umieszczać kodu w code-behind).

Najlepszym rozwiązaniem jest stworzenie usługi odpowiedzialnej za wyświetlanie komunikatów a następnie załadowanie jej z kontenera. Interfejs reprezentujący takową usługę mógłby wyglądać następująco:

interface IMessageBoxService
{
    Result Show(string message);
}

Z kolei przykładowe implementacje:

class RealMessageBoxService: IMessageBoxService
{
    public Result Show(string message)
    {
        MessageBox.Show(message);
        return Result.OK // w tym miejscu powinnismy zmapowac wynik zwrocony przez MessageBox do wlasnego enum.
    }
}
class MockBoxService: IMessageBoxService
{
    public Result Show(string message)
    {
        return Result.OK;
    }
}

Następnie w ViewModel powinniśmy skorzystać z kontenera (IoC, np. UnityContainer) aby uzyskać instancję usługi:

IMessageBoxService service = container.Resolve<IMessageBoxService>();

Dzięki wstrzyknięciu zależności, możemy w łatwy sposób przełączać się między prawdziwą implementacją wyświetlającą komunikat a zwykłym stubem. Podsumowując z przedstawionego w tym poście podejścia zyskujemy:

  1. Niezależność od bibliotek warstwy prezentacji (System.Windows itp.)
  2. TESTOWALNOŚĆ – wyświetlając komunikaty bezpośrednio w VM pozbawieni jesteśmy możliwości skorzystania z testów jednostkowych (testy jednostkowe wymagające interakcji użytkownika?- zaprzeczenie głównym zasadom). W przypadku usług, możemy wstrzyknąć zwykły MOCK object i przetestować kod – nawet w przypadku skomplikowanych worflow’ów w których logika biznesowa zależy od interakcji użytkownika.
  3. Jedno miejsce w kodzie w którym można umieszczać metody odpowiedzialne za wyświetlanie najpopularniejszych okien dialogowych. W końcu można stworzyć dodatkowe metody typu ShowWelcomeText itp. w klasie implementującej usługę.

MVP–Visual C#

Dzisiaj zostałem wyróżniony tytułem Microsoft Most Valuable Professional w kategorii Visual C#. Dla mnie to przede wszystkim dodatkowa motywacja, która przyda się po ostatniej przerwie związanej z przeprowadzką. W nadchodzących postach i artykułach będę chciał się skupić przede wszystkim na aplikacjach typu desktop a coraz mniej na web. Na blogu więcej informacji znajdziecie w przyszłości o WPF, wzorcach projektowych a niestety mniej o samym ASP.NET MVC. W następnym cyklu artykułów będę opisywał  m.in. wzorzec MVVM oraz PRISM. Do następnego postu!

mvplogo1