Pokazywanie postów oznaczonych etykietą MVVM. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą MVVM. Pokaż wszystkie posty

poniedziałek, 7 maja 2012

Tworzenie bootstrappera aplikacji przy użyciu CaliburnMicro

Witam po długiej przerwie. W dzisiejszym wpisie postaram się krótko opisać w jaki sposób stworzyć bootstrapper aplikacji przy użyciu CaliburnMicro. Ponadto przedstawię w jaki sposób skonfigurować bootstrapper w taki sposób, aby Caliburn wykorzystywał nasz własny kontener IOC.

1. Wstęp

CaliburnMicro jest to framework MVVM, który w znacznym stopniu ułatwia i przyśpiesza pisanie aplikacji pod WPF,Silverlight,WindowsPhone oraz WinRT. Zdecydowałem się poznać ten framework z uwagi na jego przenośność na różne platformy. Wcześniej całkiem sporo czasu poświęciłem Prismowi, jednakże z powodu licznych problemów z jego działaniem pod WindowsPhonem chciałem spróbować czegoś nowego.

2. Bootstrapper - Silverlight

Bootstrapper jest to ogólnie rzecz biorąc klasa inicjalizująca całą aplikację. W trakcie odpalania bootstrappera na ogół konfiguruje się kontener IOC(rejestruje wszystkie potrzebne typu), inicjalizuje się połączenie z serverem, oraz odpala się główne okno aplikacji - tzw. Shella. Dlatego też musimy odchudzić plik App.xaml.cs i zostawić w nim jedynie konstruktor wraz z wywołaniem funkcji InitializeComponents()
 public partial class App : Application
    {
        public App()
        {          
            InitializeComponent();
        }
    }
Zacznijmy od utworzenia nowego projektu typu SilverlightApplication, do którego dodajemy referencje do dll-ek Cliburna. Możemy to zrobić poprzez NuGetta (niestety u mnie one nie działały :D), jak również możemy je ściągnąć z oficjalnej strony projektu http://caliburnmicro.codeplex.com/releases/view/81466. Następnie stwórzmy główne okno aplikacji(wspomnianego wcześniej Shella) oraz jego viewmodel. W tym celu dodajemy do projektu Silverlight User Control o nazwie ShellView
oraz nową klasę ShellViewModel.
Dorzućmy jeszcze jakiegoś TextBox-a do naszego ShellView, tak aby mieć pewność, że rzeczywiście odpowiednie okno jest widoczne przy starcie aplikacji
<UserControl x:Class="Loginv2.ShellView"
    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:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro" mc:Ignorable="d" Height="516" Width="792">

    <Grid x:Name="LayoutRoot"  >
        <TextBox Text="oto jest shell"/>
    </Grid>
</UserControl>
W kolejnym kroku musimy stworzyć właściwy bootstrapper - dodajmy nową klasę o nazwie SilverlightBootstrapper, która dziedziczyć będzie po klasie Bootstrapper
namespace Client.Silverlight
{
    public class SilverlightBootstrapper : Bootstrapper<ShellViewModel>
    {
      
    }
}
Klasa Bootstrapper jest to klasa dostarczona przez CaliburnMicro, generyczny parametr T określa nam viewmodel na podstawie którego framework będzie wyszukiwał odpowiedni widok shella ze swojego wbudowanego kontenera IOC (Caliburn podczas uruchomienia aplikacji rejestruje dostępne typu w kontenerze). Zgodnie z domyślną konwencją, CaliburnMicro jako główne okno aplikacji ustawi widok, który nazywać się będzie ShellView. W celu odpalenia naszego bootstrappera musimy jeszcze umieścić go w zasobach aplikacji. Zmodyfikujmy zatem plik App.xaml aby wyglądał w następujący sposób
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:Silverlight="clr-namespace:Client.Silverlight" x:Class="Client.Silverlight.App"
             >
    <Application.Resources>
        <Silverlight:SilverlightBootstrapper x:Key="Bootstrapper"></Silverlight:SilverlightBootstrapper>
    </Application.Resources>
</Application>
Kompilując, a następnie uruchamiając nasz projekt naszym oczom powinien ukazać się następujący widok


2.1 Własny kontener IOC

Powyżej przedstawiłem w jaki sposób stworzyć najprostszą wersję bootstrapera z wykorzystaniem CaliburnMicro oraz jego domyślnego kontenera IOC. Jednakże najczęściej jest tak, że w aplikacji wykorzystujemy już jakiś bardziej zaawansowany kontener i po prostu nie chcemy wykorzystywać jednego kontenera do "rozwiązywania" widoków, a drugiego do pozostałych rzeczy. Dlatego też pokaże teraz w jaki sposób skonfigurować napisany wcześniej bootstrapper tak aby widoki były wyciągane z naszego kontenera IOC. Zacznijmy do dodania do naszego projektu referencji do NInjecta, a następnie utwórzmy klasę IOCContainer (będącą naszym customowym kontenerem), wyglądającą w następujący sposób
public class IOCContainer
{
        private static readonly IKernel Kernel = new StandardKernel();

        protected IOCContainer()
        {

        }

        public static void Dispose()
        {
            Kernel.Dispose();
        }

        public static object Get(Type type)
        {
            return Kernel.Get(type);
        }

        public static object Get(Type type,string key)
        {
            return Kernel.Get(type, key);
        }
  
        public static IEnumerable<object>  GetAll(Type instanceType)
        {
            return Kernel.GetAll(instanceType);
        }
  
        public static void Bind<T, TS>() where TS : T
        {
            Kernel.Bind<T>().To<TS>();
        } 
    }
Następnie w klasie SilverlightBootstraper musimy przeciążyć funkcję Configure,GetAllInstances oraz GetInstance. Jako, że będziemy korzystali z naszego własnego kontenera w funkcji Configure rejestrujemy wszystkie potrzebne nam viewmodele oraz widoki. Zatem funkcja Configure powinna od teraz wyglądać tak
protected override void Configure()
        {
            IOCContainer.Bind<ShellView,ShellView>();
            IOCContainer.Bind<ShellViewModel,ShellViewModel>();
        }
Następnie musimy "pokazać" Caliburn-owi gdzie powinien szukać widoków. Dlatego też przeciążamy funkcje GetInstance oraz GetAllInstances i zmieniamy ich postać na następującą
protected override object GetInstance(Type serviceType, string key)
        {
            return IOCContainer.Get(serviceType, key);
        }

        protected override IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return IOCContainer.GetAll(serviceType);
        }
Od tej pory, za każdym razem gdy użyjemy mechanizmów Caliburna do bindowania viewmodelu z widokiem itp.,Caliburm będzie szukał widoków oraz viewmodeli w naszym kontenerze IOC.

2.2 Własna konwencja wyszukiwania widoków

W przedstawionym powyżej przykładzie bootstrappera, nasz bootstrapper korzystał z domyślnej konwencji rozwiązywania widoków na podstawie viewmodeli (do ShellViewModel został dopasowany widok ShellView). Czasem jednak konwencja ta, nie pasuje do konwencji przyjętej w danym projekcie. Sam biorę udział w projekcie, w którym widoki w kontenerze są rejestrowane następujący sposób
IOCContainer.Bind< IView<ViewModel>,View>
Czy zatem oznacza to, że nie mogę korzystać już mechanizmów Caliburna i musze zrezygnować chociażby z bootstrappera ?Oczywiście,że nie. Framework dostarcza nam możliwość zdefiniowania własnej konwencji wyszukiwania widoków na podstawie viewmodeli. W celu zastąpienia domyślnej konwencji musimy podpiąć się pod propercję LocateForModelType znajdującą się w klasie ViewLocator i zdefiniować własną funkcję wyszukującą widok na podstawie viewmodelu. W moim przypadku wygląda to w następujący sposób
ViewLocator.LocateForModelType = (modelType, displayLocation, context) =>
                                                 {

                                                     string viewTypeName = string.Format("{0}[[{1}]],{2}", typeof(IView<>).FullName, modelType.AssemblyQualifiedName, typeof(IView<>).Assembly.FullName);
                                                     Type viewType = Type.GetType(viewTypeName);
                                                     if (viewType == null)
                                                         throw new ArgumentNullException(string.Format("Nie odnaleziono typu widoku dla viewModelu"), modelType.FullName);

                                                     return ViewLocator.GetOrCreateViewType(viewType);
                                                 };
Funkcja ViewLocator.LocateForModelType odpalana jest za każdym razem, gdy CaliburnMicro chce wyszukać widok na podstawie viewmodleu. Najważniejszym argumentem funkcji LocateForModelType jest modelType - czyli typ viewmodelu, który posłuży nam do znalezienia widoku - a właściwie jego typu. Po odnalezieniu typu widoku, wywołujemy funkcję
ViewLocator.GetOrCreateViewType(viewType);
(które z kolei znajdzie nam w kontenerze pożądanym przez nas typ widoku) i zwracamy jej rezultat.

3. Bootstrapper - Windows Phone

W przypadku bootstrappera dla Windows Phona sytuacja wygląda odrobinę inaczej. W celu utworzenia własnego bootstrappera musimy rozszerzyć klasę PhoneBootstrapper. Utwórzmy zatem nowy projekt typu Windows Phone Application, oraz dodajmy do niego nową kontrolkę typu Windows Phone Portrait Page.
Podobnie jak w przypadku projektu Silverlightowego dodajemy referencję do CaliburnMicro ,dorzucamy klasę viewmodelu (którą nazywamy ShellViewModel) oraz dodajemy jakiegoś textboxa do ShellView. Następnie musimy utworzyć nasz bootstrapper, dodajmy zatem klasę WindowsPhoneBootstrapper rozszerzającą klasę PhoneBootstrapper. Podobnie jak w przypadku bootstrappera silverlightowego przeciążamy funkcje odpowiedzialne za konfigurację kontenera IOC.
public class WindowsPhoneBootstrapper : PhoneBootstrapper
    {
        protected override void Configure()
        {
          
           IOCContainer.BindToConstant<INavigationService>(new FrameAdapter(RootFrame));
           IOCContainer.BindToConstant<IPhoneService>(new PhoneApplicationServiceAdapter(RootFrame));
           IOCContainer.BindAsSingleton<ShellViewModel, ShellViewModel>();
        }

      
        protected override void OnExit(object sender, EventArgs e)
        {
            IOCContainer.Dispose();
            base.OnExit(sender, e);
        }

        protected override object GetInstance(Type serviceType, string key)
        {
            return IOCContainer.Get(serviceType, key);
        }

        protected override IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return IOCContainer.GetAll(serviceType);
        }

        protected override void BuildUp(object instance)
        {
            IOCContainer.Inject(instance);
        }
    }
Zwróćmy uwagę, że w kontenerze IOC zostały zarejestrowane usługi nawigacji Windows Phona, do których został przekazany RootFrame. RootFrame jest to główne okno aplikacji, zostaje ono utworzone przez CliburnMicro. Tak samo jak w przypadku projektu Silverlightowego w pliku App.xaml.cs zostawiamy jedynie konstruktor z wywołaniem funkcji InitializeComponents(). W celu ustawienia ShellView jako RootFram-a musimy zmodyfikować plik WMAppManifest.xml (znajdujący się w katalogu Properties). Odnajdujemy tam wpis
<DefaultTask  Name ="_default" NavigationPage="MainPage.xaml"/>
i zamieniamy go na
<DefaultTask  Name ="_default" NavigationPage="ShellView.xaml"/>
Jak widać WindowsPhone wymusza tutaj podejście ViewFirst. Przy uruchomieniu aplikacji mechanizm nawigacji WindowsPhona odpali widok ShellView (bez wiedzy Caliburn'a), natomiast CaliburnMicro dopasuje odpowiedni ViewModel do naszego widoku - na podstawie domyślnej konwencji. Jeżeli chcielibyśmy zmodyfikować sposób wyszukiwania ViewModeli do widoków musimy podpiąć się do propertisa LocateForViewType znajdującego się w klasie ViewModelLocator
 ViewModelLocator.LocateForViewType = type =>
                                                     {
                                                         return //tutaj zwroc odpowiedni viewmodel na podstawie parametru type ;
                                                     };
W ostatnim kroku musimy jeszcze tylko dodać nasz bootstrapper do zasobów aplikacji
<Application.Resources>
        <Bootstrapper:WindowsPhoneBootstrapper x:Key="Bootstrapper"></Bootstrapper:WindowsPhoneBootstrapper>
    </Application.Resources>
odpalając teraz program naszym oczom powinien ukazać się taki oto widok

ś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.

niedziela, 27 listopada 2011

MVVM cz.6 - Commands part 2

Tak jak wspomniałem w poprzednim wpisie, tym razem zajmiemy się tworzeniem własnej klasy implantującej interfejs ICommand .
1. Implementacja interfejsu ICommand - WPF
Klasa implementująca ICommand może wyglądać w następujący sposób

public class DelegateCommand : ICommand
{
private Action<object> execute;
private Func<object, bool> canExecute;
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;

}
}
public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (canExecute == null) return true;
return canExecute(parameter);
}
public void Execute(object parameter)
{
if (execute == null) return;
execute((object)parameter);
}
}

Klasa DelegateCommand przyjmuje w konstruktorze dwa parametry - delegaty do funkcji. Pierwszy z nich jest to delegat do funkcji, która ma się wykonać, gdy komenda może zostać wykonana.Drugi natomiast jest to delegat do funkcji, która sprawdza czy dana komenda może zostać wykonana.Ważnym elementem jest tutaj następujący kawałek kodu :

public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}

który podłącza naszą klasę do WPF-owego systemu komend.
Teraz wystarczy w ViewModelu stworzyć obiekt typu DelegateCommand przekazać do niego odpowiednie funkcje, a następnie zbindować go do widoku. Może to wyglądać w następujący sposób:

  • ViewModel


  • public ICommand AddCommand
    {
    get;
    set;
    }
    public WatchListViewModel()
    {
    AddCommand = new DelegateCommand(val => Add(), val => CanAdd());
    }

  • Widok (XAML)


  • <Button Command="{Binding AddCommand}"/>


2. Implementacja interfejsu ICommand - Silverlight
Przedstawiony powyżej przykład skompiluje się jedynie w aplikacji WPF-owej. Silverlight niestety nie posiada CommandManagera, dlatego też gdy chcemy używać komend właśnie w tej technologii musimy zmodyfikować nasz kod na następujący:

public class DelegateCommand : ICommand
{

public event EventHandler CanExecuteChanged;
private Action<object> execute;
private Func<object, bool> canExecute;
private bool previousState;
public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;

}

public DelegateCommand(Action<object> execute)
{
this.execute = execute;
}

public bool CanExecute(object parameter)
{
if (canExecute == null) return true;
bool currentState = canExecute((object)parameter);
if (currentState != previousState)
{
previousState = currentState;
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
return currentState;
}
return currentState;

}

public void Execute(object parameter)
{
if (execute == null) return;
execute((object)parameter);
}


public void RaiseCanExecute()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}

Jak widać w funkcji

public bool CanExecute(object parameter)

sami musimy zadbać o wywołanie zdarzenia

public event EventHandler CanExecuteChanged;

Ponadto dopisana została funkcja

public void RaiseCanExecute()

funkcję tą musimy wywoływać zawsze gdy zmieni się jakaś właściwość, która ma mieć wpływ na stan przycisku zbindowanego do naszej komendy.
3. Przykład
Załóżmy,że na widoku mamy przycisk edytuj, który powinien być aktywny tylko i wyłącznie wtedy, gdy zaznaczono jakiś element na gridzie. Przycisk ten jest zbindowany do komendy EditCommand. Jak już wcześniej wspomniano, z powodu braku w Silverlighcie CommandManagera sami musimy wywołać zdarzenie informujące o potrzebie zmiany stanu przycisku. Dlatego też, w ViewModelu została stworzona właściwość SelectedItem

public Department SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
NotifyPropertyChanged(() => SelectedItem);
EditCommand.RaiseCanExecute();
}
}

która reaguje na zmiany zaznaczenia na gridzie. W chwili, gdy zaznaczenie się zmienia, wywoływany jest seter z tej właściwości, w którym to wywołujemy funkcję EditCommand.RaiseCanExecute(). Dzięki czemu informujemy kontrolkę o ewentualnej potrzebie zmiany jej stanu.
4. Podsumowanie
Komendy w bardzo prosty i wygodny sposób pozwalają nam wywoływać metody bezpośrednio z ViewModelu. Niestety mają one również swoje ograniczenia:

  • reagują jedynie na zdarzenie Click,

  • można je wykorzystać jedynie na kontrolkach dziedziczących po klasie ButtonBase oraz kontrolkach typu MenuItem

MVVM cz.6 - Commands part 1

1. Wstęp
Każdy programista, który napisał chociaż parę linijek kody w WinFormsach wie, że kontrolki używają eventów do powiadamiania o zmianach swojego stanu. W przypadku, gdy interesuje nas odpowiednia reakcja na zdarzenie, podpinamy się do niego odpowiednią funkcją i wykonujemy założone przez nas operację. W analogiczny sposób można postępować w WPF-ie oraz w Silverlighcie, jednakże podejście takie niejako mija się z modelem MVVM. Posiadanie event handlerów w kodzie, ściśle wiąże nam widok (XAML) z code behind lub nawet z samym ViewModelem danego widoku. Ponadto używanie eventów jest problematyczne (a właściwie niemożliwe), gdy widoki zostaną zdefiniowane w Resourcach. Dodatkową wadą używania event handlerów jest fakt, że aplikacje w których widok jest mocno związany z codebehind nie należą do aplikacji, które w łatwy sposób można poddać testom automatycznym.
2. Interfejs ICommand
W celu rozluźnienia połączenia między widokiem a ViewModelem wprowadzono interfejs

public interface ICommand
{

event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}

Funkcja

public bool CanExecute(object parameter)

ma za zadanie sprawdzić, czy daną komendę można wykonać. Jeżeli nie,funkcja zwraca false i kontrolka zbindowana do danej komendy jest wygaszana (właściwość IsEnabled danej kontrolki ustawiana jest na false). Z kolei funkcja

public void Execute(object parameter)

jest to odpowiednik event handlera podłączonego do zdarzenia. Zdarzenie

public event EventHandler CanExecuteChanged;

informuje widok o tym czy dana komenda może się wykonać czy też nie - dzięki temu kontrolka wie kiedy się odświeżyć(zmienić wartość właściwości IsEnabled).

3. Sposób użycia
Załóżmy, że mamy okno na którym znajduje się przycisk. Po naciśnięciu przycisku chcielibyśmy aby wykonała się jakaś czynność. Każdy obiekt typu Window oraz Control posiada kolekcję CommandBindings, która przechowuje wszystkie komendy danego okna.
W celu dodania nowej komendy należy zrobić następującą rzecz. Po pierwsze musimy stworzyć nową komendę. Robimy to przy pomocy następującego kodu:

private static RoutedCommand AddCommand = new RoutedCommand();

W celu dodania komendy do danego okna/kontrolki wykonujemy następującą operację

this.CommandBindings.Add(new CommandBinding(AddCommand,ExecuteAddCommand,CanExecuteAddCommand));

Funkcja Add z klasy CommandBindingCollection przyjmuje w parametrze obiekt typu CommandBinding. Konstruktor takiego obiektu składa się z trzech parametrów:

  • RoutedCommand - obiekt typu RoutedCommand

  • delegat typu ExecutedRoutedEventHandler - jest to delegat do funkcji, która ma się wykonać w przypadku gdy dana komenda spełnia warunki do “uruchomienia”

  • delegat typu CanExecuteRoutedEventHandler - jest to delegat do funkcji, która sprawdza czy daną komendę można wykonać. W celu zabronienia wykonania komendy należy ustawić argument ExecutedRoutedEventArgs CanExecute na false.


Na koniec należy jeszcze zbindować naszą komendę z przyciskiem. Przykładowy kod dodawania komendy może wyglądać w następujący sposób.

public partial class WatchList : Window
{
private static RoutedCommand AddCommand = new RoutedCommand();
public WatchListViewModel viewModel = new WatchListViewModel();
public WatchList()
{
InitializeComponent();
this.CommandBindings.Add(new CommandBinding(AddCommand, ExecuteAddCommand, CanExecuteAddCommand));
}

private void CanExecuteAddCommand(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = viewModel.CanAdd();}

private void ExecuteAddCommand(object sender, ExecutedRoutedEventArgs args)
{
viewModel.Add();
}
}

Bindowanie w przycisku do komendy może wyglądać w następujący sposób (XAML)


gdzie local jest aliasem na namespace, w którym znajduje się okno.
4. Podsumowanie
Użycie komend w pewnym stopniu oddzieliło nam widok od codebehind-a, jednakże wciąż nie jest to rozwiązanie idealne. Z codebehind-a wywołujemy metody ViewModelu, więc niejako ciągle jesteśmy bezpośrednio zależni od widoku. W części drugiej zaprezentuję w jaki sposób można stworzyć własny obiekt implementujący interfejs ICommand, dzięki czemu ograniczymy do minimum ilość kodu pisanego w codebehind.

MVVM cz. 5 - DependencyProperty

DependencyProperty jest to właściwość z dodatkowymi możliwościami. Definiujemy ją w klasie dziedziczącej po DependencyObject, ponieważ potrzebujemy dostępu do metod tej klasy GetValue i SetValue. Definicja DependencyProperty wygląda mniej więcej tak:
public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register("MyProperty", typeof(bool),
typeof(MyType), new PropertyMetadata(OnMyPropertyChanged));

private static void OnMyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyType)
{
// do something
}
}

public bool MyProperty
{
get { return (bool)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}

Pierwszy argument w statycznej metodzie Register to nazwa naszej właściwości i musi być taka sama jak prawdziwa nazwa MyProperty. Następnie rejestrujemy typ właściwości oraz typ naszej klasy. Dwa ostatnie parametry są opcjonalne.
Jeżeli chodzi o czwarty argument metody Register, to posiada on najwięcej możliwości. Może byc to klasa typu PropertyMetadata, UIPropertyMetadata lub FrameworkPropertyMetadata.
Jest to klasa zawierająca dodatkowe funkcjonalności dla DependencyProperty. W przypadku piątego argumentu jest to metoda która będzie walidować dane. Przyjmuje ona jeden paramter typu object w którym przekazuje nową wartość DependencyProperty.

private static bool Validate(object value)
{
// Validate...
return false;
}

Metoda zwraca bool i w przypadku wartości false otrzymujemy wyjątek System.ArgumentException.

W przypadku, gdy działamy mocno w warstwie UI i tworzymy dużo DependencyProperty możemy skorzystać ze snippeta propdp.

Warto pamiętać, że w przypadku wszystkich DependencyProperty dozwolona jest wartość DependencyProperty.UnsetValue, która wskazuje, że dana właściwośc nie ma przypisanej wartości w danym momencie.

Istotną rzeczą jest również różnica pomiędzy metodami SetValue oraz SetCurrentValue. Jeżeli używamy metody SetValue, a wcześniej ustawiliśmy binding do danej DependencyProperty (lub trigger), wówczas dany binding zostanie zniszczony. Sytuacji tej możemy uniknąć stosując metodę SetCurrentValue, która ustala wartość bez jakichkolwiek konsewkencji dla naszych bindingów i triggerów.

PropertyMetadata


Jak widać na rysunku powyżej UIPropertyMetadata dziedziczy po PropertyMetadata, a FrameworkPropertyMetadata dziedziczy po UIPropertyMetadata. Za pomocą PropertyMetadata możemy zdefiniować wartość domyślną DependencyProperty oraz dwa callbacki: PropertyChangedCallback i CoerceValueCallback.
public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register("MyProperty", typeof(MyType),
typeof(MyClass), new PropertyMetadata(null, OnMyPropertyChanged, OnCoerceValue));

private static object OnCoerceValue(DependencyObject d, object basevalue)
{
return null;
}

private static void OnMyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyClass) { }
}

public MyType MyProperty
{
get { return (MyType)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}

PropertyChangedCallback uruchamia się tylko i wyłącznie po zmianie wartości DependencyProperty, natomiast CoerceValueCallback uruchamiany jest przy każdej próbie przypisania nowej wartości (przed zmianą wartości DependencyProperty). Jako parametr metoda typu CoerceValueCallback przyjmuje nową wartość. Jeżeli jednak metoda ta zwróci wartość starą, to w ogóle nie dojdzie do odpalenia PropertyChangedCallback.
UIPropertyMetadata posiada dodatkowy feature - właściwość IsAnimationProhibited. Jeżeli jest ustawiona na true, wówczas nasza DependencyProperty nie może być użyta w animacjach.
Ostatnia klasa FrameworkPropertyMetadata oprócz powyższych posiada w konstruktorach jeszcze 2 parametry: defaultUpdateSourceTrigger (ustawia domyślną wartość dla UpdateSourceTrigger dla bindingu) oraz flagi typu FrameworkPropertyMetadataOptions. Jest to typ wyliczeniowy, którego naważniejsze wartości wymienione są poniżej:
  • AffectsMeasure -Zmiana wartości DependencyProperty powoduje wywołanie metody Measure.

  • AffectsArrange -Zmiana wartości DependencyProperty powoduje wywołanie metody Arrange.

  • AffectsParentMeasure - Zmiana wartości DependencyProperty powoduje wywołanie metody Measure dla obiektu Parent.

  • AffectsParentArrange - Zmiana wartości DependencyProperty powoduje wywołanie metody Arrange dla obiektu Parent.

  • AffectsRender - Zmiana wartości DependencyProperty powoduje odrysowanie kontrolki.

  • Inherits - Oznacza, że dana DependencyProperty jest dziedziczona - tzn. jeżeli wewnątrz kontrolki umieścimy drugą kontrolkę z tą samą, ale nie ustawioną DependencyProperty, to wówczas właściwość będzie odziedziczona z kontrolki wyższej. Bardzo dobrym przykładem jest właściwość DataContext, która jest dziedziczona w kontrolkach zagnieżdżonych.

  • NotDataBindable - Oznacza, że do danej DependencyProperty nie można się podpiąć (zbindować).

  • BindsTwoWayByDefault - Ustawia jako domyślny tryb bindingu TwoWay.

  • Journal - Oznacza, że dana DependencyProperty jest używana do nawigacji w aplikacji.


AttachedProperty

Jak sama nazwa wskazuje jest to właściwośc doczepiana. Doskonalym przykladem jest Grid.Row:

<Grid>
<TextBlock Text="Text w trzecim wierszu grida" Grid.Row="2"/>
<Label Grid.Row="3">Text w czwartym wierszy grida</Label>
</Grid>


Możemy odwołać się do property Grida z poziomu TextBlocka i przypisać wartość dla danego TextBlocka. Właściwośc ta ma osobną wartość dla każdego elementu. Definiuje się ją w nieco inny sposób niż tradycyjną właściwość. Na przykładzie grida wygląda to tak:

RowProperty = DependencyProperty.RegisterAttached("Row", typeof(int),
typeof(Grid), metadata, new ValidateValueCallback(Grid.IsIntValueNotNegative));

public static int GetRow(UIElement element)
{
// Some content
return (int)element.GetValue(Grid.RowProperty);
}
public static void SetRow(UIElement element, int value)
{
// Some content
element.SetValue(Grid.RowProperty, value);
}

Tak jak widać, nie stosujemy w tym przypadku zwykłego propertasa tylko dwie metody GetRow i SetRow, których nazwy muszą być utrzymane w takiej konwencji
GetProperty SetProperty i posiadać takie same sygnatury jak powyższe metody.

MVVM cz.4 - ElementName, RelativeSource, Source, Converters

Co w momencie, kiedy bindingi z ViewModelu nam nie wystarczą? Nie chcemy podpinać się do obiektu z DataContextu tylko do właściwości innej kontrolki, statycznego obiektu lub obiektu którego w ogóle nie widzimy w danym xamlu... Z takimi sytuacjami też możemy sobie poradzić. Binding posiada 3 właściwości, które nam w tym pomogą: ElementName, Source i RelativeSource. W danym bindingu można ustawić tylko jedną z tych trzech właściwości.

Poprzez właściwość Source możemy odwoływać się do obiektów statycznych i obiektów znajdujących się w resourcach. Np.:
<TextBlock Text="{Binding ., Source={x:Static System:DateTime.Now}}"/>

<TextBlock Text="{Binding XPath=Hulk, Source={StaticResource Hogan}}"/>


XPath jest alternatywą dla Path, dzięki czemu możemy bindować się do wezłów w xml’u. Wówczas w Resourcach powinniśmy mieć wpis podobny do poniższego:
<XmlDataProvider x:Key="Hogan">
<x:XData>
<Hulk>me</Hulk>
</x:XData>
</XmlDataProvider>


Właściwość ElementName wskazuje na obiekt o podanej nazwie.

<Border>
<TextBlock Text="{Binding Text, ElementName=NestedElement}"/>
</Border>

<Border>
<TextBlock Text="Big ben" Name="NestedElement"/>
</Border>


W efekcie takiego bindingu otrzymamy 2 elementy typu TextBlock z właściwością Text ustawioną na “Big ben”. W ramach danego widoku możemy odwoływać się do dowolnego elementu, który ma przypisaną nazwę. Ponadto z poziomu resourców, styli i control templatów też możemy podpinać się za pomocą nazwy elementu. Jedyne ograniczenie dla tego rodzaju bindingu jest wtedy, gdy z wnętrza widoku jednej klasy próbujemy się odwołać do elementu widoku drugiej klasy.

Ostatnią właściwością jest RelativeSource. Obiekt tego typu występuje w 4 trybach, które można ustawić za pomocą właściwości Mode:
  • Self - w tym trybie źródłem i celem bindingu jest ten sam obiekt
  • TemplatedParent - stosuje się przy tworzeniu kontrolek za pomocą ControlTemplate
  • FindAncestor - odszukiwanie obiektu źródłowego w drzewie wizualnym
  • PreviousData - ten tryb stosowany jest w różnego rodzaju listach, jeżeli potrzebujemy podpiąć się pod porzedni element.

Tryb FindAncestor jest powiązany z dwoma innymi właściwościami klasy RelativeSource: AncestorType i AncestorLevel. AncestorType wskazuje typ źródła natomiast AncestorLevel poziom na którym jest źródło. Jeżeli ustawimy te właściwości nie musimy wówczas bezpośrednio definiować trybu na FindAncestor, gdyż jest on wybierany domyślnie.

<TextBlock Text="{Binding Path=Name, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TextBlock}}" />


Klasa RelativeSource posiada 3 statyczne właściwości typu RelativeSource, które zwracają obiekty o różnych trybach RelativeSourceMode. Dzięki temu możemy zastosować zapis:
<Component Property="{Binding Property2, RelativeSource={x:Static RelativeSource.TemplatedParent}}">

<Component Property="{Binding Property2, RelativeSource={x:Static RelativeSource.PreviousData}}">

<Component Property="{Binding Property2, RelativeSource={x:Static RelativeSource.TemplatedParent}}">


Converters

Często okazuje się, że potrzebujemy przekonwertować bindowaną wartość na jakaś inną lub zmienić jej typ. Wówczas z pomocą przychodzą nam konwertery. Są to specjalne klasy, które dziedziczą po interfejsie IValueConverter, który zawiera dwie metody:

object Convert(object value, Type targetType, object parameter, CultureInfo culture);
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);


Metoda Convert przyjmuje jako pierwszy parametr wartość właściwości źródłowej i zamienia ją na wartość zgodną z naszą DependencyProperty takiego samego typu jak targetType. Metoda Convert odpala się po zmianie właściwości źródłowej (ViewModelProperty). Metoda ConvertBack działa analogicznie i robi konwersję w drugim kierunku. Uruchamia się zazwyczaj po interakcji użytkownika czyli wtedy gdy zmiana pochodzi od DependencyProperty.
<TextBlock Text="{Binding ShowText, Converter={StaticResource BooleanToVisibilityConverter}}"/>


Oczywiście każdy konwerter, którego używamy musi być zdefiniowany w Resources naszego widoku:
<SomeView.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</SomeView.Resources>


.Net dostarcza nam całą gamę gotowych konwerterów jak chociażby powyższy BooleanToVisibilityConverter, dzięki czemu nie musimy zawsze definiować własnych konwerterów.
Każdy konwerter ma właściwość ConverterParameter typu object. Za jej pomocą możemy sobie przekazać dowolną wartość do funkcji konwertujących. Jest to jednak zwykła właściwość dlatego nie możemy się do niej zbindować.
Warto zwrócić uwagę na statyczną wartość Binding.DoNothing którą może zwracać konweter.
Jeżeli ją zwrócimy, wówczas nie zajdą żadne zmiany we właściwości do której konwerter przekazuje wartość.
Istnieje jeszcze możliwość stworzenia konwertera wielowartościowego, który musi implementować interfejs IMultiValueConverte. Zawiera on dwie metody:
object Convert(object[] values, Type targetType, object parameter, CultureInfo culture);
object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture);


Tak jak widać jedyną różnicą jest to, że zamiast pojedynczej wartości mamy do czynienia w tablicą wartości.

MVVM cz 3, Komunikacja View-ViewModel - StringFormat

Bardzo ciekawym feature'm Bindingu jest StringFormat. Dzięki niemu, jeżeli chcemy w jakiś sposób sformatować bindowany text to nie musimy tworzyć dodatkowych konwerterów czy kilku obiektów typu TextBlock. Działa to praktycznie tak samo jak metoda String.Format() wywoływana z kodu. Czyli w nawiasach {} podajemy numer argumentu a pomiędzy nawiasami możemy wrzucać dowolny tekst. Przykłady:
<TextBox Text="{Binding SomeText, StringFormat='Moj text: {0}'}"/>

<TextBox Text="{Binding SomeText, StringFormat='Moj text: \{0\}'}"/>

<TextBox>
<TextBox.Text>
<Binding Path="SomeText" StringFormat="{}Moj text: {0}"/>
</TextBox.Text>
</TextBox>

Wszystkie powyższe zapisy zwrócą ten sam rezultat:

Moj text: SomeTextValue

Tak jak widać w powyższym przykładzie, w przypadku dłuższego zapisu bindingu musimy przed naszym stringiem umieścić puste nawiasy {}. Poza tym w skróconym zapisie możemy użyć ukośnika przed nawiasem, jednak nie jest to konieczne.
Istnieje też możliwość podłączenia kilku wartości które sformatujemy. W takim wypadku musimy wykorzystać MultiBinding. Różni się on tym od zwykłego Bindingu, że możemy podłączyć kilka właściwości źródłowych, które zostaną za pomocą StringFormat scalone w jeden string.

<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="czas1: {0} czas2: {1} czas3: {2}">
<Binding Path="Time1"/>
<Binding Path="Time2"/>
<Binding Path="Time3"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>

Co więcej możemy sformatować daną wartość za pomocą mechanizmu zdefiniowanego w Frameworku lub napisanym przez nas. Stosując zapis:

<TextBox Text="{Binding SomeText, StringFormat='Moj text: {0:hogan}'}"/>

tak naprawdę wywołujemy na pierwszym obiekcie SomeText metodę:

ToString(string format, IFormatProvider formatProvider)

interfejsu IFormattable z parametrem format = hogan. Takie typy jak Double czy DateTime implementują ten intefejs i mają zdefiniowane różne sposoby formatowania, dlatego łatwo nam manipulować całym zapisem. Możemy oczywiście w naszej klasie sami zaimplementować IFormattable.

<TextBox Text="{Binding Time, StringFormat=HH}"/>

<TextBox Text="{Binding Time, StringFormat=Time: {0:HH}}"/>

<TextBox Text="{Binding Time, StringFormat=Time: \{0:HH\}}"/>

<TextBox>
<TextBox.Text>
<Binding Path="Time" StringFormat="{}Time: {0:HH}"/>
</TextBox.Text>
</TextBox>

<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="czas1: {0:HH} czas2: {1:HH:mm} czas3: {2:HH:mm:ss}">
<Binding Path="Time1"/>
<Binding Path="Time2"/>
<Binding Path="Time3"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>

Tak jak widzimy w pierwszym przykładzie, jeżeli bindujemy jedną property, możemy wtedy podać do StringFormat po prostu sam parametr metody ToString.

MVVM cz. 2 - Komunikacja ViewModel-View

Bindings - czyli wiązanie ze sobą danych

W MVVM kluczowym pojęciem w komunikacji View-ViewModel są bindingi. Dzięki nim, można w łatwy sposób połączyć jakąś właściwość komponentu graficznego z właściwością ViewModelu. Binding jest normalną klasą, która definiuje sposób wiązania danych. Ograniczeniem bindingów jest to, że za ich pomocą możemy podpinać się jedynie do szczególnego rodzaju właściwości - DependencyProperty (opisane w oddzielnym punkcie niniejszego opracowania). Po drugiej stronie wiązania podpinamy zwykłą właściwość, która daje znać Bindingowi, że się zmieniła poprzez mechanizm notyfikacji (INotifyPropertyChanged). Jeżeli nie zadbamy o to, żeby podnieść event PropertyChanged po stronie ViewModelu to właściwość kontrolki nie zmieni się.

Schemat połączenia:

Target View.DependencyProperty <==> Source ViewModel.Property



Oczywiście zamiast ViewModelProperty istnieje też możliwość podłączenia drugiej DependencyProperty. Wówczas nie musimy się martwić o implementację interfejsu INotifyPropertyChanged. DependencyProperties zawierają alternatywny mechanizmem notyfikacji.
W xamlu jedyną właściwością obiektu klasy Binding, którą musimy zdefiniować, jest Path. Jest to nazwa właściwości źródła (w MVVM źródło to ViewModel czyli obiekt znajdujący sięw DataContexcie:)).
Przykłady Bindingów:
<Component DependencyProperty="{Binding Path=ViewModelProperty}"/>

<Component DependencyProperty="{Binding ViewModelProperty}"/>

<Component>
<Component.DependencyProperty>
<Binding Path="ViewModelProperty"/>
</Component.DependencyProperty>
</Component>

Właściwość Path jest domyślna (i opcjonalna - powyżej dwa pierwsze przykłady), dlatego nie musimy jej podawać. Wszystkie powyższe zapisy są równoważne. Ostatni zapis jest najdłuższy, dlatego w praktyce stosuje się go np z wielowartościowymi konwerterami (opisanymi w oddzielnym punkcie), przy których nie można zastosować skróconego zapisu.
Warto w tym miejscu wspomnieć o bindigu przez ".". Tzn. Jeżeli zapiszemy:
<Component DependencyProperty="{Binding .}"/>

To znaczy, że naszą źródłową property będzie cały obiekt znajdujący się w DataContexcie, czyli w przypadku MVVM będzie to cały ViewModel. Domyślnie powyższy zapis jest równoważny z:
<Component DependencyProperty="{Binding}"/>

Jednak gdy stosujemy zapis:
<Binding Path="."/>

Musimy podać “.” gdyż w przeciwnym razie otrzymamy exception ;P Dobrym zwyczajem jest zawsze wpisywać kropkę i wtedy wiadomo, że wszystko będzie działało:)

W wersji 4 framework'a nie ma możliwości debugowania bindingów [taka możliwość jest już w Silverlighcie 5 więc najprawdopodobniej pojawi się w kolejnej wersji WPF]. Jeżeli coś źle powiązaliśmy informacje o tym wyświetlane są tylko w okienku Output, do którego warto zaglądać. Kolejnym sposobem sprawdzenia jaka wartość przychodzi do DependencyProperty widoku jest stworzenie specjalnego konwertera w którym można postawić break pointa (patrz oddzielny punkt na temat konwerterów).

W praktyce wielokrotnie podpinając bindingi do widoku, okazuje się, że coś nie działa a debug i okienko output nie skutkują. Często w takim wypadku okazuje się, że zapomnieliśmy o ustawieniu właściwego trybu Bindingu (property Mode). Istnieje kilka trybów połączeń:

  • "OneWay" - ViewModel.Property może zmieniać DependencyProperty, ale nie na odwrót

  • "TwoWay" - ViewModel.Property może zmieniać DependencyProperty i odwrotnie

  • "OneWayToSource" - DependencyProperty może zmieniać ViewModel.Property, ale nie na odwrót

  • "OneTime" - DependencyProperty jest jedynie inicjowana za pomocą ViewModel.Property, a następnie właściwości nie synchronizują się

  • "Default" - wybierany jest tryb domyślny


Każda DependencyProperty może mieć domyślnie ustawiony różny tryb bindingu, jednak zazwyczaj jest to OneWay. Domyślny tryb bindingu definiuje się przy definiowaniu DependencyProperty (patrz punkt nt. DependencyProperty).

Przykład:
<Component DependencyProperty="{Binding ViewModelProprety, Mode=TwoWay}"/>

Kolejną właściwością bindingu (często nadużywaną) jest UpdateSourceTrigger. Może ona przyjmować 4 wartości:

  • Default - domyślny sposób odświeżania ViewModel.Property

  • Explicit - rzadko kiedy wykorzystuje się ten sposób odświeżania. Dzięki niemu nasza ViewModelProperty odświeży się tylko gdy z poziomu code behind odpalimy metodę UpdateSource na obiekcie klasy BindingExpression (obiekt BindingExpression możemy wydobyć za pomocą metody GetBindingExpression klasy BindingOperations)

  • PropertyChanged - W momencie kiedy nasza DependencyProperty się zmieni, wówczas odświeżana jest też property po drugiej stronie bindingu. Jest to najczęstszy przypadek.

  • LostFocus - ViewModelProperty odświeża się gdy tracimy focus na kontrolce do której się podpięliśmy.


Domyślnie większość DependencyProperty odświeża binding na PropertyChanged, jednak w przypadku np. TextBoxa, wiązanie do TextProperty odświeży się dopiero jak TextBox straci focus. W tym przypadku czasami warto przestawić tą właściwość z LostFocus na PropertyChanged.
Czasami możemy wykorzystać system asynchronicznego bindingu. Jeżeli we właściwości ViewModelu z nieznanych przyczyn zaszyliśmy jakąś logikę, która wykonuje się dość długo wówczas zawieszamy cały UI i użytkownik może być zdezorientowany. Nasz program zamula. Możemy wtedy ustawić w Bindingu property IsAsync na true. Wówczas logika ta wykona się asynchronicznie w stosunku do wątku UI, a na ekranie pokaże się wartość którą ustawiamy za pomocą FallbackValue property. Np:
<Component Property="{Binding ViewModelProperty, IsAsync=True, FallbackValue={x:Null}}"/>

Nie jest to najlepsza praktyka, raczej powinno się unikać stosowania tego rozwiązania. Property w ViewModelu powinny być "lekkie" i wykonywać się odrazu.

MVVM - wstęp

Wzorzec MVVM (Model-View-ViewModel) jest to wzorzec projektowy, który pomaga odseparować logikę biznesową od interfejsu użytkownika (separation of concerns). Dzięki takiemu podejściu zdecydowanie prościej można testować,rozwijać oraz modyfikować naszą aplikację. Wzorzec MVVM składa się z trzech zasadniczych elementów:

  • Model - klasy biznesowe

  • View - enkapsuluje interfejs użytkownika oraz jego logikę

  • ViewModel - enkapsuluje logikę biznesową


Każdy z widoków powinien posiadać własny ViewModel, który będzie się z nim komunikował poprzez DataBinding , Commands lub Behaviors (Prism).
Schemat aplikacji opartej o MVVM przedstawia poniższy rysunek:

DataContext

DataContext jest to właściwość zdefiniowana w każdym obiekcie wywodzącym się z klasy FrameworkElement.DataContext jest również domyślnym źródłem danych z których korzysta binding. Dlatego też do DataContext-u przypisywany jest zwykle obiekt ViewModelu.

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