Aplikace pro více zařízení (3.) – XAML

Vítejte v miniseriálu, který vám má pomoci psát kód tak, aby byl, aby byl co nejvíce znovupoužitelný v různých typech aplikací. V prvním díle jsme se zabývali potřebným nástrojem, což jsou zejména portable libraries. Ve druhém díle jsme se věnovali základnímu návrhovému vzoru – MVVM, zejména prostřední vrstvě ViewModel. Dnes se podíváme podrobněji na platformově závislou nejvyšší vrstvu View.

XAML a jeho rozšíření

Pro  vytváření vlastního uživatelského rozhraní se používá jazyk XAML, tedy deklarativní popis uživatelského rozhraní XML formátem. Jeho hlavní silnou stránkou je výborná podpora pro data binding. XAML dnes můžete najít:

  • na desktopu jako WPF
  • ve Windows 8 (Windows Store aplikace)
  • na telefonu s OS Windows Phone
  • jako Silverlight plug-in v prohlížeči (tento směr dnes ale není preferován)

XAML bohužel není na těchto platformách zcela stejný – vzhledem k různému formátu zařízení, stylu práce, navigace apod. jsou pro každou platformu relevantní jiné ovládací prvky. Navíce např. v oblasti data bindingu podporují různé platformy různě širokou sadu vlastností.

Nicméně nenechte se vystrašit. XAML jako vrstva View v návrhovém vzoru MVVM se sice vytváří pro každý typ aplikace zvlášť, ale velké části XAMLu lze přenášet prostým kopírováním a navíc budete na všech platformách používat stejné postupy.

Nastavení datového kontextu

View používá ViewModel jako svůj datový kontext, aby mohl provádět data binding. Nejjednodušší způsob nastavení se nabízí přímo v konstruktoru View:

public MainPage()
{
InitializeComponent();
this.DataContext = new MainPageViewModel();
}

Často se ovšem jeden ViewModel používá pro více View (například jedno pro prohlížení a druhé pro editaci dat), potom tento postup není použitelný. Dalo by se to samozřejmě řešit statickou proměnnou, ale existuje elegantnější způsob pomocí třídy ViewModelLocator, jak je popsáno např. zde. Tato speciální třída zpřístupňuje instanci každého typu ViewModelu jako svoji vlastnost. V aplikačních prostředcích pak zadefinujeme instanci této třídy:

<Application.Resources>
<vm:ViewModelLocator x:Name="ViewModelLocator"/>
</Application.Resources>

V XAMLu příslušného View pak stačí nastavit:

<Page …kráceno… DataContext="{Binding Source={StaticResource ViewModelLocator}, Path=ShoppingListViewModel}">

Jedná se tedy o velmi čisté deklarativní řešení bez použití jakéhokoliv kódu ve View.

Vazba na data

Vytvoření vlastní vazby na data je velmi jednoduché. Postačí místo konkrétní vlastnosti prvku použít složené závorky a klíčové slovo Binding, např. takto:

<GridView ItemsSource="{Binding ShoppingList}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">

Pro správnou funkci je třeba věnovat pozornost též dalším atributům třídy Binding, ale pozor – ne všechny jsou dostupné na všech XAML platformách. Osobně využívám zejména tyto:

  • Mode – pěkná past pro nováčky – na většině je vazba jednosměrná, tudíž změna hodnoty ovládacího prvku nezmění vlastnost ViewModelu, řešení je snadné – Mode=TwoWay
  • Converter – mimořádně užitečná věc. Je to třída implementující rozhraní IValueConverter pro konverzi mezi různými typy. Můžete tak například převádět vlastnost typu bool na viditelnost (Visibility) anebo datum na barvu (např. pro červené zvýraznění faktur po datu splatnosti). Kód je izolován do samostatné třídy a vyhnete se tak použití code-behind
  • UpdateSourceTrigger – určuje, kdy se změna ve View přenese na ViewModel. Typicky využijete, pokud chcete validovat prvek pro vstup textu po každé změně hodnoty. Škoda, že není na všech platformách.
  • StringFormat – pro jednoduché situace týkající se např. čísel nebo datumů se můžete vyhnout konvertorům s využitím formátování textu standardními prostředky. Opět – škoda, že není na všech platformách.

Kompletnější příklad naleznete např. zde.

Vazba na příkazy

Pro přenášení akcí uživatele nad ovládacími prvky ve View do ViewModelu, který vykoná skutečnou akci se používají příkazy, tedy objekty implementující vlastnost ICommand. Abyste nemuseli pro každý jeden příkaz definovat novou třídu, je výhodné použít třídu RelayCommand z MVVMLight. ICommand má pouze dvě důležité metody:

  • Execute je výkonný kód vykonaný po spuštění akce
  • CanExecute vrací hodnotu typu bool, která určuje, zda lze v daném stavu příkaz vykonat

Typický příklad implementace příkazu pomocí RelayCommand může být např. tento:

private RelayCommand _deleteItemCommand;
public RelayCommand DeleteItemCommand
{
get
{
if (_deleteItemCommand == null)
_deleteItemCommand = new RelayCommand(
async () => await DeleteItemAsync(),
() => this.SelectedItem != null);
return _deleteItemCommand;
}
}

Použití v XAMLu už je velmi jednoduché, stačí provést databinding vlastnosti Command na příslušnou vlastnost ViewModelu, např. takto:

<Button …kráceno… Command="{Binding DeleteItemCommand}"/>

„Akční“ ovládací prvky (tlačítka, menu, …) tuto vlastnost mají, vyvolání akce (stisk tlačítka) pak vykoná příslušnou metodu Execute. Navíc je příslušný prvek zakázaný pokud metoda CanExecute nevrátí hodnotu true, čímž se zabrání stisknutí tlačítka v okamžiku, kdy příslušná akce není platná. Aby tento mechanismus správně fungoval, je třeba při každé změně ViewModelu, která by mohla mít vliv na platnost akce příkazu vyvolat událost ICommand.CanExecuteChanged, např. zavoláním RelayCommand.RaiseCanExecuteChanged(), čímž se dosáhne aktualizace příslušného ovládacího prvku podle aktuálního stavu příkazu.

Co když ale ovládací prvek nemá vlastnost Command? Anebo pokud chci příkaz vyvolat při nějaké nestandardní události, např. pouhým najetím myši nad příslušný ovládací prvek? Existují dva způsoby jak to řešit. Pragmatickým a jednodušším způsobem je obsloužit příslušnou událost pomocí code-behind kódu, který metodu Execute příslušného příkazu vykoná. Architektonicky čistší je deklarativní způsob, např. využitím extenze EventToCommand z MVVMLight – takto vytvořený XAML je ale hůře čitelný, proto osobně oba způsoby považuji za srovnatelně dobré.

Více informací k tématu nabízí např. tento článek

Komunikace mezi vrstvami a objekty

V reálné aplikaci je často nutné spolupracovat mezi vrstvami anebo mezi různými objekty v téže vrstvě. Celý problém by šlo řešit vzájemným předáváním referencí na druhé objekty, např. v konstruktoru při vytváření nových objektů. Nicméně tento přístup špatně škáluje s počtem objektů – stává se rychle nepřehledným a obtížně udržovatelným. Aplikace je pak navíc velmi obtížně testovatelná.

Proto MVVM frameworky většinou nabízí možnost volné vazby mezi objekty pomocí dobře známého publish/subscribe přístupu. Ten umožňuje vyvíjet a testovat jednotlivé části (například objekty ViewModelu) v izolaci, umožňuje komunikovat mezi vrstvami, např. z vrstvy ViewModel do vrstvy View, ale přitom nezvniká kompilační závislost ViewModelu na View – tato by byla naprosto nežádoucí!

Realizace tohoto principu je velmi jednoduchá, ve frameworku MVVMLight se pro ni používá třída Messenger. Nejprve si zadefinujeme typ posílané zprávy, např.

public class RefreshMessage
{
public bool IsNewUser { get; set; }
}

Pokud chce některá komponenta reagovat na tuto zprávu, předplatí si ji v jednom řádku kódu:

Messenger.Default.Register<RefreshMessage>(this, OnRefreshMessageAsync);

Odeslání zprávy jinou komponentou je rovněž triviální:

Messenger.Default.Send(new RefreshMessage() { IsNewUser = true });

Více informací lze nalézt např. v tomto článku.

Code behind pro XAML?

Zajímavou teoretickou otázkou s velkým praktickým dopadem je: Jaký kód je a není přípustný jako tzv. XAML code behind ve vrstvě View?

Obecná odpověď je poměrně jednoduchá – je žádoucí, aby ve vrstvě View bylo kódu co nejméně. Tato vrstva se obtížně testuje, tudíž by mělo jít pouze o kód, ve kterém “nelze udělat chybu :-)”. K tomu, aby bylo kódu co nejméně míří i řada technik zde uvedených – používání výše uvedených technik jako je data binding, konvertory, ViewModelLocator, Command, EventToCommand – to vše vede ke snižování až elimininaci potřebného kódu ve vrstvě View.

Je ale cílem, aby byl code-behind úplně prázdný, jak někteří fundamentalisté požadují? Žádoucí to asi sice je, ale spíše je vhodné se chovat pragmaticky. Malý code-behind je zjevně menší zlo, než velmi krokolomná konstrukce, která ho eliminuje.

Kdy se např. code-behind hodí:

  • Pro čistě vizuální operace, které nemají nic společného s logikou aplikace – např. změna velikosti prvku nebo nastavení focusu.
  • Obejítí chybějící schopnosti UpdateSourceTrigger, pokud chcete ViewModel změnit po každém úhozu klávesy v ovládacím prvku TextBox.
  • Vykonání příkazu nad ViewModelem při nestandardní události anebo u prvku, který nemá vlastnost Command (lze obejít použitím EventToCommand, ale výsledek je poněkud rozvleklý)

Naopak určitě by code-behind neměl:

  • Obsahovat jakoukoliv aplikační logiku – ta patří do ViewModelu
  • “Prozrazovat” ViewModelu implementační detaily svého View – například mu předávat reference na ovládací prvky apod.- tím by vznikaly nežádoucí závislosti nižší vrstvy na vyšší

Vzorové řešení

Opět jako příklad použiji jednoduchou, ale zcela funkční aplikaci, jejíž kód dám ke stažení v posledním díle seriálu. Ve třetím kroku implementujeme vrstvu View, která je platformově závislá:

image

Implementaci v tomto případě provádíme opakovaně (zde dvakrát) v každém platformovém projektu. Naštěstí je množství kódu poměrně velmi malé:

  • V deklaraci aplikace App.xaml je třeba deklarovat ViewModelLocator
  • Vytvořit dva typy View pro Windows 8 aplikaci (LoginPage, MainPage)
  • Vytvořit tři typy View pro Windows Phone aplikaci (LoginPage, MainPage, AddItemsPage). V tomto případě MainPage a AddItemsPage používají stejný ShoppingListViewModel. Důvodem rozdělení je menší velikost obrazovky na telefonu. XAML pro obě platformy je ovšem velmi podobný.

Pokud jde o code-behind pro XAML, je využit pouze velmi sporadicky:

  • Na stránce MainPage pro Windows Store aplikaci nahrazuje chybějící tlačítko Back (Windows Phone jej má hardwarové).
  • Na stránce AddItemsPage pro Windows Phone aplikaci je přidáno pro větší uživatelský komfort softwarové tlačítko pro navigaci zpět (i když Windows Phone jej má hardwarové).
  • Na stránce MainPage je obsluha tlačítek na pruhu aplikace. Tato tlačítka totiž nejsou vykreslena v XAMLu a nepodporují data binding, tuto nepříjemnost je nutno obejít jednoduchým kódem využívajícím ViewModel.

Závěr a pozvánka

Dne 9.4. od 10:00 hodin pořádáme na výše uvedené téma hodinový online seminář, kde se o problematice můžete dozvědět více. Využijte příležitosti položit konkrétní dotazy, které vás zajímají. Zaregistrovat se můžete zde.

V posledním díle seriálu se budeme zabývat možnostmi abstrakce platformy.

Michael