Coproject – demo RIA aplikace krok za krokem, díl 13
Pokud jste přečetli celý seriál o Coprojectu, jistě už používáte Caliburn.Micro na svých projektech. Všechno, co jste se zatím mohli o C.M dočíst je užitečné, nicméně, pokud vyvíjíte větší aplikace, jistě jste narazili na pár komplikací, které vás donutily C.M mírně upravit nebo rozšířit. Přesně z toho důvodu bych chtěl pokračovat v seriálu poněkud komplikovanějšími věcmi, se kterými jsme se při používání C.M setkali. Připomínám, že tento článek je českou adaptací originálu, který vyšel na blogu společnosti Baud.
Typický problém, na který při používání C.M u větších aplikací narazíte, je to, že celá struktura aplikace (tj. jednotlivé Conductory / ViewModely) jsou načteny ihned po spuštění aplikace. To je v pořádku u malých projektů, ovšem u aplikace s desítkami různých stránek to může způsobit problém s alokací paměti. V tomto díle bych chtěl vyzkoušet řešení pojmenované LazyScreen.
Lazy Screen
Hlavní myšlenka spočívá v tom, že běžné ViewModely zabalíme do prvku LazyScreen, který se bude navenek chovat jako ViewModel, tudíž ho bude možné zobrazit v menu a podobně, ale vnořený ViewModel načte, až když bude potřeba. Díky tomu bychom také měli být schopni vnořené ViewModely zavírat a později zase otevírat. Tak můžeme uvolnit paměť zavřením obrazovek, které právě nepoužíváme. C.M sice podporuje zavírání i běžných ViewModelů, jenže v tom případě zavřený VM úplně zmizí z toho nadřazeného a tím i například z menu. My naopak chceme, aby se sice VM zavřel, ale v menu zůstal a po vybrání z menu se opět načetl.
ModuleMetadata
Pojďme tedy vyzkoušet, jestli se nám podaří popsanou funkcionalitu implementovat. Otevřete si zdrojové kódy Coprojectu (můžete je stáhnout z Codeplexu).
Pokud budeme chtít, aby se nám načetly moduly (Home, Messages, To Do, Milestones) až po vybrání z menu, musíme jejich popisky přemístit z instancí (property DisplayName) na jiné místo, které bude dostupné ještě před vytvořením dané instance. K tomu nám opět poslouží metadata. Otevřete tedy IModuleMetadata a přidejte property Title:
public interface IModuleMetadata { int Order { get; } string Title { get; } }
Tip: Víte, že klávesovou zkratkou CTRL+, rychle přejdete na jakýkoliv objekt ve zdrojových kódech? Navigace dokonce funguje jen podle velkých písmen, takže stačí napsat IMM, abyste našli IModuleMetadata.
Pak upravte ExportModuleAttribute, aby implementoval rozšířený interface:
[MetadataAttribute] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class ExportModuleAttribute : ExportAttribute, IModuleMetadata { public int Order { get; private set; } public string Title { get; private set; } public ExportModuleAttribute(int order, string title) : base(typeof(IModule)) { Order = order; Title = title; } }
A na závěr opravte i definice jednotlivých modulů, aby používaly nový konstruktor atributu ExportMetadata. Například HomeViewModel by měl vypadat takto:
[ExportModule(10, "Home")] public class HomeViewModel : Screen, IModule
Celá solution by se teď měla dát zase zkompilovat.
LazyScreen
Teď, když máme připravená metadata, je na čase pustit se přímo do LazyScreen. Ve složce Framework vytvořte třídu LazyScreen a upravte její definici takto:
public class LazyScreen: Screen
K tomu, aby naše třída LazyScreen otevřela vnořený ViewModel až tehdy, když o to bude požádána, použijeme třídu ExportFactory, která je součástí MEFu. Ta funguje přesně tak, jak byste od ní čekali – na požádání vytvoří MEF export (takže v podstatě vrátí novou instanci komponenty z MEFu jako DI kontejneru). Navíc umí vrátit také metadata, aniž by bylo nutné vytvořit konkrétní instanci komponenty.
Konstruktor tedy bude vypadat následovně:
private ExportFactory_factory; public LazyScreen(ExportFactory factory) { _factory = factory; }
A zbytek hlavní funkcionality:
private TScreen _screen; private ExportLifetimeContext_export; private object _lock = new object(); public TMetadata Metadata { get { return _factory.Metadata; } } public bool IsScreenCreated { get { return _export != null; } } public TScreen Screen { get { lock (_lock) { if (!IsScreenCreated) { _export = _factory.CreateExport(); _screen = _export.Value; } return _screen; } } }
public void Reset() { if (!IsScreenCreated) { return; } lock (_lock) { _export.Dispose(); _export = null; _screen = default(TScreen); } NotifyOfPropertyChange(() => IsScreenCreated); NotifyOfPropertyChange(() => Screen); }
Když se podíváte na přidaný kód, zjistíte, že se celá logika opravdu točí jen okolo toho, aby při prvním požadavku na property Screen došlo k vytvoření nové instance z ExportFactory, která byla předána hned při vytvoření v konstruktoru. Funkce Reset pak zase obsah property screen maže.
Abychom mohli vyzkoušet, že nápad s LazyScreen opravdu funguje, musíme ještě upravit shell. Otevřete tedy ShellViewModel a upravte ho následovně:
[Export(typeof(IShell))] public class ShellViewModel : Conductor>.Collection.OneActive, IShell { [ImportingConstructor] public ShellViewModel([ImportMany]IEnumerable > moduleHandles) { var modules = from h in moduleHandles orderby h.Metadata.Order select new LazyScreen (h); Items.AddRange(modules); } }
Všimněte si, že jsme jen upravili typ parametru konstruktoru (z Lazy na ExportFactory), vytvořili z něj nové instance LazyScreen pro kolekci Items a podle toho pak upravili generický typ bázové třídy.
Teď už zbývají jen drobné změny v ShellView:
- Změňte binding textboxu v listboxu Items z DisplayName na Metadata.Title
- Změňte jméno prvku ActiveItem_Description na ActiveItem_Screen_Description
- Změňte jméno prvku ActiveItem na ActiveItem_Screen
Význam těchto změn by měl být jasný – jelikož je mezi shellem a moduly ještě jedna vrstva (náš LazyScreen), musíme tyto prvky v ShellView přesměrovat až na vnořený ViewModel (v property Screen).
Když teď zkompilujete a spustíte aplikaci, zjistíte, že funguje stejně jako předtím. Ano, nevypadá to jako velký pokrok, ale berme to jako potvrzení toho, že zvolený přístup s LazyScreen může fungovat. Navíc už teď dochází k tomu, že se každý jednotlivý ViewModel modulu načte až poté, co jste ho vybrali v menu.
Refaktoring
Na kódu, který jsme zatím napsali, se mi nelíbí jeden řádek – jde o opakování generických parametrů v “select new LazyScreen …” v konstruktoru ShellViewModelu. Abychom se ho zbavili, využijeme toho, že v případě generických funkcí umí kompilátor automaticky odhadnout generické parametry podle typu parametrů funkce. Přidejte do LazyScreen.cs tento kód:
public static class LazyScreen { public static LazyScreenCreate ( ExportFactory factory) { return new LazyScreen (factory); } }
Teď můžeme upravit konstruktor ShellViewModelu takto:
var modules = from h in moduleHandles orderby h.Metadata.Order select LazyScreen.Create(h);
Nejde sice o žádné zásadní zlepšení, ale kód je teď čitelnější a tento trik by se vám mohl někdy hodit.
Dokončení LazyScreen
Když se nám podařilo ukázat, že koncept LazyScreen funguje, měli bychom dotáhnout jeho implementaci až do konce. Půjde hlavně o předání některých volání na vnořený ViewModel. Následují funkce pro aktivaci a deaktivaci:
protected override void OnActivate() { ActivateScreen(); } protected override void OnDeactivate(bool close) { DeactivateScreen(close); } private void ActivateScreen() { var activatableScreen = _screen as IActivate; if (activatableScreen != null) { activatableScreen.Activate(); } } private void DeactivateScreen(bool close) { var deactivatableScreen = _screen as IDeactivate; if (deactivatableScreen != null) { deactivatableScreen.Deactivate(close); } }
Funkce spojené se zavíráním ViewModelů:
public override void CanClose(Actioncallback) { var closableScreen = _screen as IGuardClose; if (closableScreen != null) { closableScreen.CanClose(callback); } else { base.CanClose(callback); } } public new void TryClose() { var closableScreen = _screen as IClose; if (closableScreen != null) { closableScreen.TryClose(); } base.TryClose(); }
A pak ještě pro vztah rodič/vnořený ViewModel:
public override object Parent { get { return base.Parent; } set { base.Parent = value; if (IsScreenCreated) { SetScreenParent(); } } } private void SetScreenParent() { var childScreen = _screen as IChild; if (childScreen != null) { childScreen.Parent = this.Parent; } }
Teď už jen zbývá upravit getter vlastnosti Screen:
if (!IsScreenCreated) { _export = _factory.CreateExport(); _screen = _export.Value; SetScreenParent(); if (IsActive) { ActivateScreen(); } }
A přidat tento řádek na začátek bloku lock ve funkci Reset:
DeactivateScreen(true);
Tím je náš LazyScreen hotov. Zapojili jsme ho do naší aplikace, vnořené ViewModely se načítají až po otevření nadřazeného LazyScreen a jinak vše funguje tak, jako dříve. V příštím díle se podíváme na LazyConductor, který by měl umožnit také jednotlivé LazyScreen zavírat (volat funkci Reset).
Otázka
Řešili jste už podobný problém s načítáním ViewModelů až na vyžádání? Jak? Co říkáte na navrhované řešení?
Nabídka
Pokud vás série o Coprojectu zaujala, nebojíte se učit nové věci a chtěli byste se mnou a dalšími lidmi ve společnosti Baud pracovat na současných i nových projektech, ozvěte se na augustin.sulc(at)baud.cz. Hledáme nové kolegy jak pro oblast vývoje SW,tak i pro databáze. Na vzdělání nebo dosavadních zkušenostech nám tolik nesejde – přijďte si s námi radši popovídat!
Komentáře