MVVM – #2 – MVVM Toolkit #1

Tak jak zapowiedziałem w poprzednim wpisie traktującym o wzorcu MVVM, chciałbym przedstawić MVVM Toolkit – narzędzie, które znacznie ułatwia korzystanie z tego wzorca. Oczywiście w sieci dostępnych jest wiele innych bibliotek, które także mają za zadnie wspomagać programistów przy implementacji tego wzorca. Jednymi z najpopularniejszych są: Prism oraz Caliburn Micro. Nie będę zagłębiał się w różnice pomiędzy nimi a MVMM Toolkit – chętnych odsyłam do stron tych projektów.

Wstęp

Przedstawiając MVVM Toolkit, stworzymy prostą aplikację obliczającą rachunek w restauracji:

  • będziemy wyświetlać listę dostępnych pozycji w menu restauracji wraz z ceną
  • wybrane pozycje będą dodawane na listę zamówienia
  • koszt zamówienia będzie na bieżąco obliczany
  • w nowym oknie umożliwimy wyświetlenie podsumowania zamówienia

Tak jak wcześniej zapowiadałem, będziemy korzystać z platformy WPF. Do dzieła!

 

Przygotowania

Zacznijmy od stworzenia nowego projektu aplikacji WPF. Następnie za pomocą NuGet’a pobierzemy i zainstalujemy MVVM Toolkit. Wystarczy, że w Visual Studio wybierzemy Tools->NuGet Package Manager->Package Manager Console a następnie w wyświetlonej konsoli wpiszemy polecenie:

Install-Package MvvmLight

Po chwili narzędzie zostanie zainstalowane. Narzędzie możemy także pobrać, ze strony projektu.

 

Struktura projektu

Po udanej instalacji, struktura projektu uległa zmianie:



MVVM Toolkit utworzył folder ViewModel a w nim klasy MainViewModel oraz ViewModelLocator. Gdy otworzymy plik z klasą MainViewModel spostrzeżemy, że dziedziczy ona po ViewModelBasemusimy dziedziczyć właśnie po tej klasie, gdy tworzymy nowe klasy ViewModeli.

Po otwarciu pliku klasy ViewModelLocator i usunięciu komentarzy naszym oczom ukaże się następujący kod:

using GalaSoft.MvvmLight.Ioc;
using Microsoft.Practices.ServiceLocation;

namespace Restauracja.ViewModel
{
    public class ViewModelLocator
    {
        public ViewModelLocator()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

            SimpleIoc.Default.Register<MainViewModel>();
        }
        
        public MainViewModel Main
        {
            get
            {
                return ServiceLocator.Current.GetInstance<MainViewModel>();
            }
        }
        
        public static void Cleanup()
        {
            // TODO Clear the ViewModels
        }
    }
}

 

Zadaniem tej klasy jest zarządzanie naszymi ViewModelami. W konstruktorze konfigurujemy najpierw statyczną klasę ServiceLocator, następnie rejestrujemy wcześniej utworzony model widoku MainViewModel w tzw. kontenerze IoC(Inversion of Control) . Kontentery IoC są tematem na oddzielny artykuł, natomiast w skrócie służą one do tworzenia obiektów „na żądanie” oraz do zarządzania nimi. Dzięki nim zmniejszamy zależności pomiędzy klasami w projekcie, ponieważ nie musimy ich tworzyć sami – wystarczy, że zażądamy od kontenera obiektu takiego jakiego potrzebujemy.  Kilka linijek niżej istnieje właściwość Main, której zadaniem jest zwrócenie modelu widoku, który całkiem niedawno zarejestrowaliśmy w konstruktorze klasy ViewModelLocator.

Za każdym razem gdy dodamy nowy model widoku do projektu, powinniśmy zrobić dwie rzeczy:

  1. Zarejestrować go w kontenerze IoC, w konstruktorze ViewModelLocator
  2. Dodać publiczną właściwość, która zwróci tam ten model widoku

 

 

Widok

W porządku, teraz zajmijmy się widokiem. Początkowa struktura pliku XAML wygląda następująco:

<Window x:Class="Restauracja.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Restauracja"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        
    </Grid>
</Window>

 

Nasuwa się pytanie: Jak podłączyć widok do naszego modelu widoku?. Odpowiedź brzmi: za pomocą DataContext. W klasie ViewModelLocator istnieje właściwość, która zwróci nam gotowy model widoku wystarczy, że o niego poprosimy. Dodajmy kontekst danych dla widoku:

DataContext="{Binding Source={StaticResource Locator}, Path=Main}"

 

W kontekście danych odwołujemy się  do statycznego zasobu o identyfikatorze Locator. Gdzie on się znajduje? Po poprawnej instalacji MVVMToolkit, w pliku App.xaml, został dodany zasób Locator(jeżeli z jakiegoś powodu, nie masz zdefiniowanego tego zasobu – dodaj go):

<Application x:Class="Restauracja.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Restauracja" StartupUri="MainWindow.xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d1p1:Ignorable="d" xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006">
  <Application.Resources>
    <ResourceDictionary>
      <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" xmlns:vm="clr-namespace:Restauracja.ViewModel" />
    </ResourceDictionary>
  </Application.Resources>
</Application>

 

Teraz po podpięciu modelu widoku, nasz plik XAML wygląda następująco:

<Window x:Class="Restauracja.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Restauracja"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
    <Grid>
        
    </Grid>
</Window>

 

Zdefiniujemy teraz widok w pliku XAML, póki co bez data-bindingu. Tym zajmiemy się po napisaniu modelu widoku. Zakładam, że znasz podstawy budowania widoków w XAML.

<Window x:Class="Restauracja.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Restauracja"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto" />
            <RowDefinition  Height="*" />
        </Grid.RowDefinitions>
        <Grid VerticalAlignment="Stretch" Height="Auto">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Label>MENU</Label>
            <ListView Grid.Row="1" x:Name="restaurantMenu" SelectionMode="Single"/>
            <Button Grid.Row="2">Dodaj</Button>
        </Grid>
        <Separator Grid.Column="1" Grid.RowSpan="3">
            <Separator.LayoutTransform>
                <RotateTransform Angle="90" />
            </Separator.LayoutTransform>
        </Separator>

        <Separator Grid.Row="1" Grid.ColumnSpan="3" />

        <StackPanel Grid.Column="2" Orientation="Vertical">
            <StackPanel Orientation="Horizontal">
                <Label Width="40">Koszt</Label>
                <Label x:Name="cost"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Label Width="40">Ilość</Label>
                <Label x:Name="count"/>
            </StackPanel>
        </StackPanel>

        <Grid Grid.Row="2">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Label>Wybrane</Label>
            <ListView Grid.Row="1"/>
            <Button Grid.Row="2">Czyść</Button>
        </Grid>
        <DockPanel Grid.Column="2" Grid.Row="3">
            <Button DockPanel.Dock="Bottom" Height="50" Margin="10"
                    Content="PODSUMOWANIE">

            </Button>
        </DockPanel>

    </Grid>
</Window>

 

Świetnie! Mamy gotową strukturę widoku. Zanim przejdziemy do modelu widoku, stwórzmy sam model.

 

 

Model

Naszym modelem będzie lista potraw, napoi i przekąsek dostępnych w restauracji. Stworzymy taką listę „na sztywno”. Dodajmy zatem do projektu nowy folder o nazwie Model, a w nim dodajmy klasę RestaurantMenuModel oraz DishModel. RestaurantMenuModel będzie modelem, który przechowuje menu, natomiast DishModel jest jedną z jego pozycji.

Zawartość RestaurantMenuModel:

public class RestaurantMenuModel
{
    private List<DishModel> _restaurantMenu = new List<DishModel>
        {
            new DishModel
            {
                Name = "Pierś z kaczki",
                Price = 37
            },
            new DishModel
            {
                Name = "Pieczone żeberka",
                Price = 34
            },
            new DishModel
            {
                Name = "Łosoś",
                Price = 32
            },
            new DishModel
            {
                Name = "Zupa pomidorowa",
                Price = 8
            },
            new DishModel
            {
                Name = "Szarlotka",
                Price = 12
            },
            new DishModel
            {
                Name = "Kawa",
                Price = 11
            },
            new DishModel
            {
                Name = "Zielona herbata",
                Price = 9
            },
            new DishModel
            {
                Name = "Napój gazowany",
                Price = 9
            }
        };

    public List<DishModel> RestaurantMenu
    {
        get { return _restaurantMenu; }
        set
        {
            if (value != null)
                _restaurantMenu = value;
        }
    }
}

 

DishModel:

public class DishModel
{
    public string Name { get; set; }
    public double Price { get; set; }

    public override string ToString()
    {
        return $"{Name}, {Price}zł";
    }
}

 

W DishModel przeładowujemy metodę ToString, będzie nam potrzebna do poprawnego wyświetlenia pozycji w kontrolce ListView w widoku. Teraz gdy dodaliśmy model, możemy przejść do modelu widoku.

 

 

Model widoku

Do wypełnienia kontrolki zawierającej pozycje menu, będziemy potrzebowali kontenera, który takie pozycje będzie przechowywał. Ważną kwestią przy tworzeniu modelu widoku jest zadbanie o powiadamianie o zmianach w danych, które udostępniamy dla widoku. Widok musi zostać poinformowany za każdym razem gdy z poziomu modelu widoku zmienimy dane. Jeżeli chodzi o kontenery, z pomocą przychodzi nam kolekcja ObservableCollection, która implementuje interfejs INotifyCollectionChanged dzięki, któremu widok będzie informowany, że nastąpiła zmiana w danych i wie, że powinien odświeżyć swoją zawartość. Dodajmy zatem  kolekcję ObservableCollection oraz pole z referencją do modelu:

public class MainViewModel : ViewModelBase
{
    private RestaurantMenuModel _restaurantMenuModel;
    public ObservableCollection<DishModel> RestaurantMenu { get; }


    public MainViewModel()
    {
        _restaurantMenuModel = new RestaurantMenuModel();
        RestaurantMenu = new ObservableCollection<DishModel>(_restaurantMenuModel.RestaurantMenu);

    }
}

 

Teraz zajmijmy się dodaniem właściwości odpowiedzialnych za przechowywanie ilości oraz kosztu wybranych pozycji menu. Dodajmy zatem dwie właściwości:

public class MainViewModel : ViewModelBase
{
    private RestaurantMenuModel _restaurantMenuModel;
    public ObservableCollection<DishModel> RestaurantMenu { get; }


    public MainViewModel()
    {
        _restaurantMenuModel = new RestaurantMenuModel();
        RestaurantMenu = new ObservableCollection<DishModel>(_restaurantMenuModel.RestaurantMenu);

    }
}

 

Jeżeli w takiej postaci podepniemy je pod widok, po aktualizacji wartości np. kosztu, w widoku nie zauważmy zmiany. Musimy poinformować widok o zmianie. Tu z pomocą ponownie przychodzi MVVM Toolkit. Zmodyfikujmy wcześniej dodane właściwości:

private int _count;
private const string CountPropertyName = "Count";
public int Count
{
    get { return _count; }
    set
    {
        Set(CountPropertyName, ref _count, value);
    }
}

private double _cost;
private const string CostPropertyName = "Cost";
public double Cost
{
    get { return _cost; }
    set
    {
        Set(CostPropertyName, ref _cost, value);
    }
}

 

Zauważ, że w setterze właściwości wywołujemy metodę Set klasy ViewModelBase. Metoda ta wewnętrznie wywołuje metodę RaisePropertyChanged, opakowuje ona funkcjonalność interfejsu INotifyPropertyChanged. Dzięki czemu widok zostanie poinformowany o zmianie wartości.

Dodajmy potrzebne wiązania w widoku:

<StackPanel Grid.Column="2" Orientation="Vertical">
    <StackPanel Orientation="Horizontal">
        <Label Width="40">Koszt</Label>
        <Label x:Name="cost" Content="{Binding Cost}"  />
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <Label Width="40">Ilość</Label>
        <Label x:Name="count" Content="{Binding Count}"/>
    </StackPanel>
</StackPanel>

 

W porządku, teraz dodajmy możliwość wybierania pozycji z menu. Do tego zatwierdzania wyboru użyjemy przycisku „Dodaj”, który znajduje się w lewym górnym rogu widoku. Zamierzamy po zaznaczeniu wybranej pozycji i kliknięciu przycisku, dodać ją do listy poniżej. Jak to zrobić? Użyjmy komendy!

 

 

Komendy

Od teraz obowiązuje złota zasada: „W widoku nie odwołujemy się do żadnych metod w modelu widoku, a w code – behind staramy się nie dodawać żadnego kodu lub staramy się to zminimalizować to do minimum”

Komenda jest uniwersalnym mechanizmem to reagowania na interakcję użytkownika z interfejsem.  Od teraz gdy klikniemy przycisk wywołamy odpowiednią komendę w modelu widoku, istnieje także możliwość przekazywania danych do takiej komendy.

MVVM Toolkit dostarcza wygodną implementację komend w postaci klasy RelayCommand, która opakowuje interfejs ICommand, dając wygodny sposób tworzenia nowych komend, bez potrzeby zagłębiania się w ich implementację. Standardowa implementacja komendy nie przyjmującej parametru wygląda następująco:

private RelayCommand _myCommand;
public RelayCommand MyCommand
{
    get
    {
        return _ myCommand
            ?? (_myCommand = new RelayCommand(
            () =>
            {
                //zadanie do wykonania

            }));
    }
}

 

Natomiast z parametrem wygląda następująco:

private RelayCommand<TYP_PARAMETRU> _myCommand;
public RelayCommand<TYP_PARAMETRU> MyCommand
{
    get
    {
        return _myCommand
               ?? (_myCommand = new RelayCommand<TYP_PARAMETRU>(
                   parameter =>
                   {

                   }));
    }
}

 

Dodajmy zatem naszą komendę do modelu widoku:

private RelayCommand<DishModel> _addItemCommand;
public RelayCommand<DishModel> AddItemCommand
{
    get
    {
        return _addItemCommand
            ?? (_addItemCommand = new RelayCommand<DishModel>(
            (item) =>
            {
                SelectedDishes.Add(item);

            }));
    }
}

 

Teraz dodajmy nową listę, której zadaniem będzie przechowywanie dodanych pozycji:

public ObservableCollection<DishModel> SelectedDishes { get; } = new ObservableCollection<DishModel>();

 

Następnie dodajmy źródło danych dla kontrolki listy:

<StackPanel Grid.Row="2">
    <Label>Wybrane</Label>
    <ScrollViewer VerticalAlignment="Stretch">
        <ListView ItemsSource="{Binding SelectedDishes}" />
    </ScrollViewer>
</StackPanel>

 

Parametrem komendy jest DishModel, ponieważ taki typ dodawaliśmy do listy menu w widoku. Przypiszmy stworzoną komendę do przycisku „Dodaj”:

<Button Command="{Binding AddItemCommand}"
        CommandParameter="{Binding ElementName=restaurantMenu, Path=SelectedItem}">Dodaj</Button>

 

Zwróć uwagę, że nadaliśmy nazwę kontrolce listy, abyśmy mogli się do niej odwołać. Parametrem komendy przypisanej do przycisku jest aktualnie wybrana pozycja z listy. Teraz gdy uruchomisz aplikację, wybrane pozycje będą dodawały się do drugiej kontrolki listy! Wspaniale!

 

Koszt oraz ilość

Zajmijmy się teraz wyliczaniem kosztu oraz ilości. Zadanie jest banalne! Wystarczy, że zaktualizujemy ciało komendy, którą niedawno dodaliśmy. Pomożemy sobie przy użyciu LINQ:

public RelayCommand<DishModel> AddItemCommand
{
    get
    {
        return _addItemCommand
            ?? (_addItemCommand = new RelayCommand<DishModel>(
            (item) =>
            {
                SelectedDishes.Add(item);

                int count = SelectedDishes.Count;
                double cost = SelectedDishes.Sum(dish => dish.Price);

                Count = count;
                Cost = cost;

            }));
    }
}

 

Nasza aplikacja poprawnie już wylicza koszt oraz ilość wybranych pozycji! Obsłużmy jeszcze czyszczenie zamówienia. Wystarczy, że dodamy nową bezparametrową komendę:

private RelayCommand _clearCommand;
public RelayCommand ClearCommand
{
    get
    {
        return _clearCommand
               ?? (_clearCommand = new RelayCommand(
                   () =>
                   {
                       SelectedDishes.Clear();
                       Count = 0;
                       Cost = 0;

                   }));
    }
}

 

Oraz powiążemy ją z przyciskiem:

<Button Command="{Binding ClearCommand}">Czyść</Button>

 

Od teraz bez problemu, możemy dodawać nowe pozycje do zamówienia, usunąć zamówienie oraz poprawnie obliczyć koszt oraz ilość pozycji w zamówieniu.

 

To działa!

Stworzyliśmy prostą aplikację z wykorzystaniem wzorca MVVM oraz narzędzia MVVM Toolkit. Pomimo prostoty, udało nam się przebrnąć przez kilka ważnych kwestii  m.in. komendy oraz wiązanie danych. W widoku pozostał nam jeden przycisk, z którym nic nie zrobiliśmy. Dodanie obsługi tego przycisku będzie tematem kolejnego wpisu poświęconego MVVM. Przedstawię w nim sposób komunikacji pomiędzy modelami widoku z wykorzystaniem obiektu Messenger, wchodzącego w skład MVVM Toolkit.

Mam coś dla Ciebie

Zapisz się do mojego newslettera, a ja prześlę Ci zbiór kilkunastu praktycznych wskazówek dla programisty aplikacji mobilnych.

Menu