Coproject – demo RIA aplikace krok za krokem, díl 10.
V tomto díle přidáme do aplikace indikátor načítání dat ze serveru.
Komunikace se serverem může nějakou dobu trvat a i v naší malé aplikaci se občas stane, že načítání dat ze serveru zabere pár sekund. Abychom uživateli zpříjemnili používání aplikace (a zvýšili vnímanou rychlost), přidáme ukazatel načítání dat.
Otevřete si tedy zdrojové kódy z minulého dílu, nebo si je stáhněte z oficiálních stránek Coprojectu na Codeplexu.
BusyIndicator
Nejprve přidáme ovládací prvek BusyIndicator do view seznamu. Otevřete tedy ToDoListsView a přidejte následující definici na začátek souboru:
xmlns:toolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
Potom vložte tento kód na konec prvku LayoutRoot (pod prvky ActiveItem a Toolbar):
<toolkit:BusyIndicator IsBusy="{Binding IsBusy}" Grid.RowSpan="2" />
Teď už jen stačí implementovat property IsBusy, na kterou je prvek navázaný.
IsBusy
Nejjednodušším způsobem implementace property IsBusy je vytvořit následující property typu bool v ToDoListsViewModelu:
private bool _isBusy; public bool IsBusy { get { return this._isBusy; } set { this._isBusy = value; NotifyOfPropertyChange(() => IsBusy); } }
Teď už jen nastavíme tuto property na True na začátku funkce LoadData a na False na jejím konci. Vyzkoušejte si to.
Pozn.: Pokud je načítání moc rychlé a vy tak vůbec nevidíte BusyIndicator, otevřete si serverový projekt a v něm do funkce GetToDoListsWithItems v CoprojectService před řádek s return vložte “Thread.Sleep(1000)”.
BusyWatcher
Způsob, který jsme si popsali výše, sice funguje, pokud by ale zároveň probíhalo více načítání, dojde k problémům:
- první načítání začne a nastaví IsBusy na True.
- druhé načítání začne a nastaví IsBusy na True.
- první načítání skončí a nastaví IsBusy na False.
- druhé načítání stále probíhá, ale IsBusy je již na False a ukazatel načítání už není zobrazen.
Jedno z možných řešení bude připravit si třídu, která bude počítat, kolik právě probíhá načítání, a na venek bude ukazovat IsBusy kladné, pokud probíhá alespoň jedno načítání. Jedna z možných implementací je následující (vložte tento kód do Framework/BusyWatcher):
public class BusyWatcher : PropertyChangedBase { int _counter; public bool IsBusy { get { return _counter > 0; } } public void AddWatch() { if (Interlocked.Increment(ref _counter) == 1) { NotifyOfPropertyChange(() => IsBusy); } } public void RemoveWatch() { if (Interlocked.Decrement(ref _counter) == 0) { NotifyOfPropertyChange(() => IsBusy); } } }
Abychom mohli opět využít MEF a Dependency Injection, vytáhneme z této třídy interface IBusyWatcher a necháme BusyWatcher toto rozhraní exportovat:
public interface IBusyWatcher { bool IsBusy { get; } void AddWatch(); void RemoveWatch(); }
[Export(typeof(IBusyWatcher))] public class BusyWatcher : PropertyChangedBase, IBusyWatcher ...
Abychom mohli tuto třídu použít v našem view modelu, musíme nahradit původní property IsBusy touto:
[Import(RequiredCreationPolicy = CreationPolicy.NonShared)] public IBusyWatcher Busy { get; set; }
Dále je potřeba nastavování této property ve funkci LoadData nahradit voláním Busy.AddWatch/RemoveWatch.
Nakonec upravte binding prvku BusyIndicator ve view takto:
<toolkit:BusyIndicator IsBusy="{Binding Busy.IsBusy}" Grid.RowSpan="2" />
Dobře, problém souběžného načítání dat máme vyřešený. Co se ale stane, když dojde k výjimce někde uvnitř funkce LoadData? Zůstane nám v počítadle viset jedna akce a ukazatel bude vidět pořád. Nabízí se jasné řešení – uzavřít celý obsah funkce LoadData do try bloku a pak do finally části dát volání RemoveWatch.
BusyTicket
Elegantnějším řešením je využití using bloku. K tomu si při zavolání AddWatch vytvoříme “ticket” implementující IDisposable a při jeho “disposování” zavoláme automaticky RemoveWatch.
Dovnitř BusyWatcheru přidejtu tuto třídu:
public class BusyWatcherTicket : IDisposable { IBusyWatcher _parent; public BusyWatcherTicket(IBusyWatcher parent) { _parent = parent; _parent.AddWatch(); } public void Dispose() { _parent.RemoveWatch(); } }
Dále do BusyWatcheru přidejte tuto funkci a dejte její definici i do rozhraní IBusyWatcher:
public BusyWatcherTicket GetTicket() { return new BusyWatcherTicket(this); }
Novou funkci využijeme takto:
public IEnumerable<IResult> LoadData(string filter) { using (Busy.GetTicket()) { CoprojectContext context = new CoprojectContext(); ...
Lists = result.Result.Entities; NotifyOfPropertyChange(() => Lists); } }
Všimněte si, že v tomto případě máme samostatný BusyWatcher pro každý view model (NonShared v importu). Jednoduše ale můžete aplikaci upravit tak, že budete mít sdílený BusyWatcher pro celou aplikaci (třeba pokud byste chtěli zobrazovat ukazatel ve statusbaru a tím používat i jeden společný BusyIndicator.
Bylo by možné busyindikator implementovat tak, že bude mít ve svém obsahu (content), to co se právě mění/načítá?
<toolkit:BusyIndicator IsBusy="{Binding Busy.IsBusy}" Grid.RowSpan="2" >
<datagrid ....>/*content changed after load finished*/</datagrid>
</toolkit:BusyIndicator>
To by určitě ělo - BusyIndicator je v podstatě jen ContentControl s předpřipraveným obsahem a rozšířený o property IsBusy s funkcí automatického schovávání, když je IsBusy false. Pak ovšem budete muset taky vymyslet, jak tu informaci o tom, co se právě děje, do něj dostanete. Osobně bych to asi řešil změnou počítadla v BusyWatcheru na nějakou kolekci "hlášek". Při získávání BusyTicketu bych pak musel předat i popis toho, co budu dělat, a BusyWatcher potom mimo IsBusy bude mít i property string CurrentActions se seznamem těchto hlášek. Pak bych navázal BusyWatcher na tuto property.
Jistě nejde o jediné (a pravděpodobně ani nejlepší :-) řešení, ale minimálně dodržuje oddělení "zodpovědností" jednotlivých komponent aplikace, což je podle mého velice důležité pro snadnou údržbu aplikace.