Coproject – demo RIA aplikace krok za krokem, díl 8.
V tomto díle vytvoříme detail prvku ToDoItem.
Rozsah DomainContextu
Při vytváření typických seznam/detail aplikací je zásadní zvážit, jaký bude rozsah jednotlivých prvků jako zdroj dat (v našem případě RIA Services DomainContextu) – tj. jestli bude jeden DomainContext sdílen seznamem a všemi detaily, nebo jestli bude mít každé okno vlastní DomainContext. Oba přístupy mají své silné i slabé stránky a nedá se obecně prohlásit, že byste měli používat pouze jeden přístup. Volba záleží na dané situaci a s největší pravděpodobností budete ve své aplikaci kombinovat oba přístupy v závislosti na tom, o jaké entity se jedná a jaký je jejich význam v aplikaci.
Sdílený DomainContext
Tento přístup se obvykle implementuje pomocí sdílené služby (service) v aplikaci, která poté jednotlivým komponentám zpřístupňuje data a zároveň se stará o další manipulaci s daty vzhledem ke zdroji (např. ukládání dat na server a podobně). Tento přístup je vhodný pro entity, které jsou využívány v celé aplikaci – může se jednat třeba o uživatelské účty.
+ Jednotlivé komponenty aplikace si mohou mezi sebou předávat přímo jednotlivé entity.
+ Když na jednom místě aplikace aktualizujete data, není nutno synchronizovat zbytek aplikace – všechny komponenty pracují se stejnou entitou, tudíž mají k dispozici aktuální data.
+ Každá entita je v paměti pouze jednou, i když může být používána na více místech aplikace.
+ K datům se přistupuje přes jedno místo.
- Entity zůstávají v paměti dokud nejsou odpojeny (detach) od zdroje (DomainContext) nebo není samotný zdroj uvolněn z paměti. Pokud tedy přes sdílený DomainContext načtete stovky entit (například pro nějaký seznam) a neodpojíte je, zůstanou v paměti i když se v seznamu přesunete na jinou stránku. Velice brzy pak budete mít problémy s množstvím potřebné paměti.
- Neuložené změny ve sdílených entitách se projeví na všech místech, kde se tyto entity také používají.
- Obtížnější ukládání – pokud uložíte celý DomainContext, můžete s tím poslat na server i zatím nedokončené změny, které mezitím právě prováděla jiná část aplikace.
- Máte horší přehled a možnosti ovlivnit, co a proč zůstává v paměti. Obzvláště vzhledem k současným memory leakům v SL4.
Samostatný DomainContext
V tomto případě každé okno aplikace (případně jiná logická jednotka) používá svůj vlastní DomainContext, který využívá k načítání i aktualizaci dat. Tento přístup využijete nejspíše u velkých seznamů a v detailech, které umožňují úpravu dat.
+ Snadná kontrola nad tím, které další části aplikace využívají danou entitu. Lépe tak dohlédnete na případné důsledky provedených úprav.
+ Jednodušší správa paměti. DomainContext můžete snadno “zahodit” a uvolnit tak paměť.
+ Jednodušší ukládání. Pokud uložíte aktuální DomainContext, máte přehled o tom, které entity budou uloženy.
- Jednotlivé části aplikace si mezi sebou musí předávat ID entit.
- V aplikaci může existovat více instancí jedné datové entity.
- Po změně dat na jednom místě musíte na ostatních místech data znovu načíst.
- Při ukládání může dojít ke konfliktům, pokud byla jedna entita změněna na více místech v aplikaci.
ToDoItemViewModel
Pro editaci entity ToDoItem použijeme přístup se samostatnými DomainContexty. Nejprve si vytvoříme nový interface ViewModels.Interfaces.IToDoItemEditor, který použijeme k označení view modelu:
public interface IToDoItemEditor { ToDoItem Item { get; } IEnumerable<IResult> Setup(int toDoItemID); }
Poté ve složce ViewModels vytvoříme vlastní view model pro detail:
[Export(typeof(IToDoItemEditor))] [PartCreationPolicy(CreationPolicy.NonShared)] public class ToDoItemViewModel : Screen, IToDoItemEditor { public ToDoItem Item { get; private set; } private CoprojectContext _context; public IEnumerable<IResult> Setup(int toDoItemID) { _context = new CoprojectContext(); var dataResult = new LoadDataResult<ToDoItem>( _context, _context.GetToDoItemsQuery().Where(x => x.ToDoItemID == toDoItemID)); yield return dataResult; Item = dataResult.Result.Entities.First(); NotifyOfPropertyChange(() => Item); } }
Jak vidíte, view model očekává pouze ID entity a zbytek dat si načte ze serveru sám. Context si uložíme, protože ho použijeme později pro uložení změn. Jelikož budeme chtít vytvořit jednu instanci ToDoItemViewModelu pro každý detail, je důležité také nastavit PartCreationPolicy na NonShared, tj. přepnout z výchozího nastavení “Singleton” na “vždy nová instance”.
Dále potřebujeme přidat logiku, která otevře detail ToDoItem. Pro každý ToDoList zobrazujeme ListBox, takže bychom mohli použít stejný postup, jakým přepínáme mezi moduly, jenže tyto ListBoxy by sdílely společný prvek SelectedItem a jelikož takový prvek bude pouze v jednom z ListBoxů, v ostatních by to způsobovalo chybu při bindování, případně jiné nečekané chování. Využiji tedy příležitosti a ukážeme si, jak konfigurovat bindování na události v Caliburn.Micro.
Nejprve otevřete ToDoListsViewModel a změňte bázovou třídu ze Screen na
Conductor<IToDoItemEditor>.Collection.OneActive
Pak přidejte následující funkci
public IEnumerable<IResult> OpenItemDetail(ToDoItem item) { var editor = Items.FirstOrDefault(x => x.Item.ToDoItemID == item.ToDoItemID); if (editor == null) { editor = IoC.Get<IToDoItemEditor>(); yield return editor.Setup(item.ToDoItemID).ToSequential(); } ActivateItem(editor); yield break; }
Teď už jen stačí zařídit, aby byla tato funkce spuštěna. Do ToDoListsView přidejte da začátek souboru tuto xmlns definici:
xmlns:cal="http://www.caliburnproject.org"
Aby byla funkce OpenItemDetail spuštěna při vyvolání události SelectionChanged, měli bychom upravit hlavičku ListBoxu navázaného na ToDoItems následovně:
<ListBox ItemsSource="{Binding ToDoItems}" cal:Message.Attach="[Event SelectionChanged] = [Action OpenItemDetail($this.SelectedItem)]" ...
Jelikož je ale událost SelectionChanged považována C.M za výchozí akci prvku ListBox, vyvolání akce (Action) je výchozí reakcí na událost a SelectedItem je výchozí property ListBoxu pro bindování, můžeme tento zápis zkrátit následovně:
<ListBox ItemsSource="{Binding ToDoItems}" cal:Message.Attach="OpenItemDetail($this)"
Parametr $this je speciální C.M klíčové slovo, za které C.M dosadí příslušnou hodnotu. Další možná slova jsou $datacontext a $eventargs a jejich význam je přesně takový, jak naznačují jejich jména. Více si o tom můžete přečíst zde.
Aby se měl vybraný detail kde zobrazit, přidejte ještě pod ScrollViewer obsahující ItemsControl s názvem “Lists” tento prvek:
<ContentControl x:Name="ActiveItem" Grid.Column="1" Grid.Row="1" Margin="10,0,0,0" HorizontalContentAlignment="Stretch" />
ToDoItemView
View model a logiku pro jeho zobrazení už máme hotovou, teď nám ještě zbývá vytvořit view. Do složky Views proto přidejte nový Silverlight User Control a pojmenujte ho ToDoItemView.
Přidejte do něj tyto definice:
xmlns:dataForm="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm.Toolkit" xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
a formulář pro editaci dat:
<dataForm:DataForm CurrentItem="{Binding Item}" HorizontalAlignment="Stretch"> <StackPanel> <dataForm:DataField PropertyPath="DueDate"> <controls:DatePicker SelectedDate="{Binding DueDate,Mode=TwoWay}" /> </dataForm:DataField> <dataForm:DataField PropertyPath="Content"> <TextBox Text="{Binding Content,Mode=TwoWay}" TextWrapping="Wrap" Height="auto" /> </dataForm:DataField> <dataForm:DataField PropertyPath="User"> <TextBlock Text="{Binding User.LastName}" /> </dataForm:DataField> </StackPanel> </dataForm:DataForm>
Když teď spustíte aplikaci, bude vypadat asi takto:
Prvky DataField automaticky přidaly správné popisky. Pokud jste již dříve pracovali s DataFormem, možná se divíte, kam zmizela typická tlačítka pro DataForm. Odpověď je v souboru Assets/Custom.xaml, kde se nachází styl, který tato tlačítka skrývá. Nicméně DataForm, ačkoliv jde o velice užitečný a potřebný prvek, stále obsahuje dost chyb a proto se budeme snažit používat ho co nejméně (respektive využijeme hlavně základní funkce jako zapnutí/vypnutí formuláře, validaci a automatické popisky). Abyste si nemysleli, že si vymýšlím, tady jsou typické chyby, se kterými se můžete setkat při používání DataFormu:
- I když přes properties vypnete používání tlačítek Commit a Cancel, tak se při změně editačního módu občas objeví.
- Při přepínání mezi DataFormy se občas stane, že zmizí první prvek ve formuláři (v našem případě popisek DueDate).
- Pokud upravíte text v nějakém TextBoxu a následně zapnete ReadOnly mód formuláře, prvky ve formuláři zůstanou editovatelné.
Jistě jste si všimli, že položka User ve formuláři je prázdná. To je způsobeno tím, že vnořená entita User se na klienta vůbec nedostane. Otevřete tedy soubor Services/CoprojectService.cs v serverovém projektu, najděte funkci GetToDoItems() a upravte ji takto:
return this.ObjectContext.ToDoItems.Include("User");
Spusťte aplikaci – teď už by mělo být vše v pořádku:
Komentáře