WPF – Caliburn.Micro Some Kind of Magic

Pod koniec poprzedniego postu na temat MVVM wspomniałem o Caliburn.Micro jako narzędziu, które czyni moją pracę lżejszą. Dla tych, którzy nie wiedzą jest to framework wspomagający w tworzeniu aplikacji opartej o wzorzec MVVM. Jeśli ktoś zastanawia się dlaczego od razu nie wspomniałem o tym frameworku i poprzedni przykład robiłem bez jego użycia to moja odpowiedź jest prosta (i poparta gorzkim doświadczeniem). Otóż robiąc coś zaawansowanego musimy mieć świadomość mechanizmów za tym stojących inaczej będziemy zastanawiać się nad oczywistymi sprawami.

Caliburn.Micro

Na początek instalujemy nugetem interesujący nas pakiet

Install-Package Caliburn.Micro

I może trochę nietypowo, ale zacznę od końca tzn. od widoku i efektu finalnego. Cały kod dostępny jest na githubie pod tagiem Caliburn.Micro_StartPoint

View

<TextBox x:Name="FirstName" />
<TextBox x:Name="LastName" />
<TextBox x:Name="Address" />
<Button x:Name="SayHi" Content="Say Hi!" />

Jak widać brak jakichkolwiek informacji o bindingu, brak informacji o obsłudze przycisku. Dodatkowo usunięta została informację o DataContext okna. Code-Behind pozostawiamy czysty. Klikamy na button SayHi a on… działa. Gdy zobaczyłem to pierwszy raz w działaniu to pierwsze co mi się skojarzyło to czarna magia (czyli coś w sam raz dla mnie).

ViewModel

Jak komuś dalej mało to teraz czas na kod ViewModelu:

public class MainWindowViewModel : PropertyChangedBase
{
    private readonly Person _person;

    public string FirstName
    {
        get { return _person.FirstName; }
        set
        {
            _person.FirstName = value;
            NotifyOfPropertyChange("FirstName");
            NotifyOfPropertyChange(() => CanSayHi);
        }
    }

    public string LastName
    {
        get { return _person.LastName; }
        set
        {
            _person.LastName = value;
            NotifyOfPropertyChange("LastName");
            NotifyOfPropertyChange(() => CanSayHi);
        }
    }

    public string Address
    {
        get { return _person.Address; }
        set
        {
            _person.Address = value;
            NotifyOfPropertyChange("Address");
            NotifyOfPropertyChange(() => CanSayHi);
        }
    }

    public MainWindowViewModel()
    {
        _person = new Person();
    }

    public void SayHi()
    {
        if (!PersonExists(_person))
        {
            MessageBox.Show(string.Format("Hi {0} {1}!", _person.FirstName, _person.LastName));
            SavePerosn(_person);
        }
        else
            MessageBox.Show(string.Format("Hey {0} {1}, you exists in our database!", _person.FirstName, _person.LastName));
    }

    private void SavePerosn(Person _person)
    {
        //Some Database Logic
    }

    public bool CanSayHi
    {
        get { return !PersonExists(_person); }
    }

    private bool PersonExists(Person _person)
    {
        //Some logic
        return false;
    }
}

Znowu zero odwołań do widoku (zgodnie ze wzorcem MVVM). Tylko teraz klasa zamiast implementować INotifyPropertyChanged z dziedziczy ze PropertyChangedBase. Chwila sprawdzenia i okazuje się, że to PropertyChangedBase implementuje INotifyPropertyChanged a więc wszystko gra. Ponadto nie ma żadnego odwołania do ICommand a klasa RelayCommand została usunięta. Szukamy więc dalej.

App.xaml

Rzadko zagląda się do tego pliku, ale to tutaj znajduję się część odpowiedzi na nurtujące nas problemy. Do momentu instalacji Caliburn.Micro plik ten wyglądał w następujący sposób

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

ale teraz jego wygląd zasadniczo się zmienił

<Application x:Class="MVVM.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MVVM"
             >
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary>
                    <local:AppBootstrapper x:Key="bootstrapper" />
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

To co pierwsze rzuca się w oczy to brak StartupUri. Nie określamy już tutaj punktu startowego aplikacji. Drugą zmianą jest określenie bootstrappera.

Bootstrapper

Od tej chwili Bootstrapper to najważniejsza klasa w naszej aplikacji. A oto jej wersja minimalistyczna:

public class AppBootstrapper : BootstrapperBase
{
    public AppBootstrapper()
    {
        Initialize();
    }

    protected override void OnStartup(object sender, StartupEventArgs e)
    {
        DisplayRootViewFor<MainWindowViewModel>();
    }
}

jest to wersja bootstrappera dla Caliburn.Micro w wersji 2.0.1. To co tu przedstawiłem to absolutne minimum niezbędne do działania aplikacji. Sam w swoich aplikacjach korzystam z bardziej rozbudowanej wersji bootstrappera, ale na to też przyjdzie pora w następnych odsłonach. Sprawne oko od razu zauważy, że to w metodzie OnStartup odbywa się teraz określenie co stanowi punkt startowy aplikacji (przy czym radzę się do tego zbytnio nie przywiązywać, gdyż w trochę inaczej zbudowanym bootsrapperze odbywa się to w innym miejscu). Jeśli ktoś chciałby już teraz zobaczyć jak może wyglądać trochę bardziej rozbudowany bootstrapper to polecam zainstalować w czystym WPF(WindowsPhonoe, Silverlight) projekcie pakiet Caliburn.Micro.Start. Poza niezbędnymi plikami odpali się strona informująca jak należy zmodyfikować App.xaml w zależności od naszych potrzeb.

Convention Over Configuration

Jeśli ktoś w tym momencie próbowałby odpalić aplikację to niestety czeka go srogi zawód. Aplikacja odpali się, ale nic się nie wyświetli. To znaczy wyświetli się taki komunikat
CannotFindViewMessage
Przyczyną tego jest nieprzestrzeganie konwencji. Otóż Caliburn.Micro wymusza stosowanie pewnych konwencji co początkowo może wydać się niepotrzebnym utrudnieniem, ale z czasem daje pewne korzyści. Takie podejście nazywa się Convention over configuration i z tego co się orientuje szczególnie bliskie jest tworzącym w RoR.
Jakiej więc konwencji nie przestrzegaliśmy? Podpowiedzią niech będzie fakt, że nie wyświetlił się widok. Otóż jeśli mamy ViewModel o nazwie MainWindowViewModel to bootstrapper będzie szukał do niego View o nazwie MainWindowView. I tyle. Gdybyśmy mieli więcej widoków w naszej aplikacji to dla lepszego poruszania się po projekcie pasuje przemieścić View i ViewModel do osobnych namespaceów (folderów). Konkretnie do folderów Views i ViewModels. Tak po prostu jest przyjęte (chyba, że napiszemy własną konwencję, ale o tym też w późniejszym terminie). Wracając do konwencji to:

  • property FirstName jest zbindowane z TextBoxem o tej samej nazwie
  • public void SayHi() z ViewModel odpowiada kliknięciu buttona SayHi w View
  • property CanSayHi odpowiada za property IsEnabled przycisku SayHi (wystarczyło aby property zaczynało się od Can i było typu bool, oczywiście niezbędne było takie ustawienie NotifyOfPropertyChange aby zmiana tekstu w textboxach odpalała property CanSayHi)

Na koniec jeszcze jedna uwaga jeżeli przez pomyłkę, któryś element z View nie miałby swojego odpowiednika w ViewModel nie spowoduje to żadnego exception (radzę więc uważać).
W następnej części opiszę bardziej zaawansowane kwestie bindowania a także jak podpiąć inne zdarzenia(lub kilka do tego samego elementu interfejsu).

10 thoughts on “WPF – Caliburn.Micro Some Kind of Magic

  1. Pingback: dotnetomaniak.pl
  2. Bardzo ciekawy wstęp do Caliburn. Najczęsciej korzysta się z Prisma i fajnie poczytać jak wygląda to w innym frameworku. Mam nadzieje że pojawią sie kolejne posty 🙂

  3. Przyznam, że z niecierpliwością oczekiwałem tego wpisu po ostatniej zapowiedzi.. i się nie zawiodłem 🙂 Mam nadzieję, że kolejny wpis poruszy temat nawigacji, bo to mnie chyba najbardziej ciekawi

  4. Czy mógłbyś odpowiedzieć jak wygląda długoterminowe wsparcie dla tych wszystkich frameworków? Ostatnio zasstanwialiśmy się z dyrektorem nad wyborem frameworku dla naszej aplikacji i ostatecznie nie wybraliśmy żadnego. Powód? Brak pewności wsparcia w okresie długoterminowym. Nasze aplikacje są utrzymywane przez lata. Dodatkowo specyfika branży (farmacja – audyty software’u, walidacje, itd) bardzo utrudnia przeprowadzanie głębokich zmian w aplikacji, np. przejście na inny framework, gdy obecny straci wsparcie. Poza tym, nasi klienci (a zwłaszcza ich managerowie IT podchodzą z dystansem do tworów typu open source).

    1. W takim przypadku użycie open source jest faktycznie problematyczne. Jeśli chodzi o wsparcie to faktycznie może być problem choć nie dotyczy on tylko open source. Za przykład niech posłuży Silverlight.

    2. Ja jakieś 2-3 lata temu sporo czasu poświęciłem na analizę różnych frameworków i ostatecznie zdecydowałem się na stworzenie własnego mechanizmu do pracy z WPF/MVVM (przy okazji zgadzam się z autorem, że WPF bez MVVM nie ma racji bytu ;)). Mechanizm ten w głównej mierze oparty jest na extension methods i refleksji. Od tamtego czasu używam go przy każdej nowej aplikacji WPF i sprawdza się doskonale. Code-behind widoków mam czysty, nie używam ICommand, nad wszystkim sam panuję i jestem niezależny od zewnętrznych narzędzi 😉 Polecam podobne podejście, a przy tworzeniu własnego mechanizmu mamy pełną swobodę i możemy go dostosować do własnych potrzeb.

      1. Nad własnym mechanizmem ma się największą kontrolę, ale nie zawsze warto wynajdywać koło na nowo. Czasem pozbycie się pewnej części niezależności może przynieść korzyści.

        1. Pełna zgoda, zawsze trzeba wybrać złoty środek. Jednak w kontekście tego co napisał Marek, nie wybranie żadnego frameworka i pisanie wszystkiego na piechotę nie jest zbyt optymalnym rozwiązaniem. W takiej sytuacji lepiej poświęcić trochę czasu na stworzenie własnych mechanizmów ułatwiających pracę z WPF – tym bardziej, że jak wspomniał, ich aplikacje utrzymywane są przez lata więc ta inwestycja na pewno się zwróci.

  5. Tak, jak piszecie prawdopodobnie zdecydujemy się na autorskie rozwiązanie. MVVM musi być, nie wyobrażam sobie tradycyjnie klepać kod pod zdarzeniami, aczkolwiek przy odrobinie dyscypliny i myślenia jest to do ogarnięcia.

Comments are closed.