Kód na formuláři

Skoro každý vývojář Dynamics AX zná pravidlo, že na formuláře nepatří aplikační logika, jenže jen málokdo ho bere alespoň trochu vážně. Je s podivem, že ač si sami programátoři stěžují na pomalé a těžko udržovatelné formuláře, nepřiměje je to začít něco dělat jinak

Myslím, že to má několik příčin:

  1. Programátoři často zcela nechápou, jaké problémy aplikační logika na formulářích přináší. Tedy sice znají pravidlo, ale nerozumí mu.
  2. Programátoři neví, jak jinak kód strukturovat. Znají pravidlo, ale neumí ho aplikovat.
  3. Nebo je kvalita ve skutečnosti nezajímá.

Problémy s kódem na formulářích

Formulář je kopírován do vrstvy jako celek

V AX2009 a starších verzích je celý formulář zkopírován do aktivní vrstvy, jakmile změníte byť jen jedinou vlastnost či metodu. To představuje obrovský problém pro upgrady a slučování kódu. Například: když změníte jednu metodu v CUS vrstvě a pak se změní jiná metoda ve VAR, tato změna se v CUS vrstvě neprojeví, dokud formulář ručně neupgradujete. Pokud by se to samé dělo ve třídě, žádný upgrade by nebyl potřeba, protože do vyšších vrstev jsou kopírovány jednotlivé metody, nikoli celý objekt.

Naštěstí AX2012 tento problém značně omezila – formuláře už se nekopírují jako celek, ale po menších částech, nicméně datové zdroje stále trpí stejným problémem.

Formulář není typ

Formulář v Dynamics AX není typ (jako třeba třídy a tabulky), je to v zásadě jen nějak sestavená instance třídy FormRun. Nelze se tedy na něj odkazovat jako na typ, což by bylo často potřeba.

Nemůžete například používat formuláře s operátory is a as. Nebo pokud chcete volat formulářovou metodu z jiného objektu, kompilátor nemůže kontrolovat, zda taková metoda existuje, což snadno vede k chybám. Mimochodem, taková volání nejsou také zohledněna v křížových referencích a na formulářové metody se nelze odkazovat ani pomocí funkcí jako methodStr() (např. při volání setTimeOut()), takže kompilátor opět nemůže kontrolovat jejich existenci.

K volání metod najdete více v mém článku Dynamické volání metod v X++.

Znovupoužitelnost kódu

Jak bylo řečeno, kód napsaný na formuláři je obtížné bezpečně volat z jiných objektů, takže každý formulář by měl obsahovat jen kód, který se vztahuje přímo k němu a nepředstavuje nějaký obecnější koncept. Veškerý kód, který by se mohl hodit i jinde, by měl být přesunut do tříd, případně tabulek, tabulkových map a podobně.

Dědičnost

Protože formulář není samostatná třída, nelze vytvořit jeho potomka a zdědit chování. To je přitom často velmi užitečné. Například:

  • Jeden formulář nabízející různé chování pro různý kontext dat. To je implementováno pomocí třídy se specializovanými potomky. Příkladem může být formulář PurchTable a jeho obslužné třídy PurchTableForm_Project a PurchTableForm_Journal.
  • V mnoha případech můžete minimalizovat zásahy do kódu existující třídy, pokud vytvoříte jejího potomka a změny chování naimplementujete v něm. V rodičovské třídě pak změníte jedinou metodu – necháte construct() vracet instanci vaší třídy. Protože jiné metody nebyly změněny, nemůže v nich vzniknout při upgradu konflikt vrstev.
  • Potomek třídy může být použitý při jednotkovém testování k překrytí některých metod (například pro simulování zdroje dat) nebo přístupu k instančním proměnným a chráněným metodám.

Pokud máte kód ve třídě, je vytvoření potomka triviální. V případě formuláře se buď musíte bez výše uvedených výhod obejít, nebo musíte nejprve kód zrefaktorovat do třídy.

Rozhraní

Často vídám formuláře,  které dělají něco jako toto:

Object caller = element.args().caller();
if (caller.name() = formStr(Form1)
    || caller.name() = formStr(Form2)
    || caller.name() = formStr(Form3))
{
    caller.doSomething();
}

Tento kód v podstatě říká, že vyjmenované formuláře podporují nějaké chování reprezentované metodou doSomething(). Pokud chceme přidat, ubrat nebo přejmenovat podporovaný formulář, je nutné tento kód změnit, se všemi důsledky jako zkopírování formuláře do aktivní vrstvy atd.

Normální objektově-orientovaný kód by použil rozhraní a vůbec by nepotřeboval výčet podporovaných objektů. Bohužel formuláře nejsou třídy a nemohou implementovat rozhraní. Můžete ale předat instanci obslužné třídy v Args.caller() nebo Args.parmEnum() a pracovat místo s formulářem s ní.

DoSomethingProvider provider = element.args().caller() as DoSomethingProvider;
if (provider)
{
    provider.doSomething();
}

Testování

Pro jakékoli automatizované testy je daleko snazší a rychlejší pracovat s třídami a metodami než s celým formulářem, hledat ovládací prvky na formuláři, číst jejich vlastnosti atd. Například, než hledat na formuláři tlačítko Post a volat jeho metodu click(), je snazší zkrátka zavolat metodu post() v obslužné třídě.

Pro testy uživatelského rozhraní je zase občas užitečné nahradit třídu se skutečnou aplikační logikou nějakou jinou implementací – ta může třeba číst testovací data z jiného zdroje nebo logovat informace o průběhu testu.

Kód na klientu

Všechny formulářové metody běží na klientu. Pokud například pošlete z klienta dotaz do databáze, je nejprve poslán na AOS, AOS ho pošle do databáze, databáze pošle odpověď na AOS a AOS ji konečně předá na klienta. Přitom každé spojení vyžaduje určitou režii, v závislosti zejména na latenci sítě a množství přenášených dat.

Ignorování tohoto faktu je nejčastějším zdrojem problémů s výkonem formulářů, protože když nejste opatrní, je snadné vygenerovat spoustu dotazů a přenášet velké množství dat.

Podrobná diskuze je nad rámec tohoto článku, ale základní pravidla se dají shrnout takto:

  1. Minimalizujte počet volání mezi jednotlivými vrstvami (klient/AOS/databáze). Pokud potřebujete více dotazů do databáze nebo na serverové objekty, neposílejte každý jednotlivý dotaz z klienta. Použijte serverovou metodu, která zavolá vše potřebné z AOS a vrátí pouze výsledek, čili volání z klienta na server a zpět bude pouze jedno.
  2. Nepropadněte snaze přesunout všechno na AOS – pokud váš kód nepotřebuje nic ze serveru, nechte ho běžet na klientu.
  3. Používejte cache. To platí zejména pro display a edit metody.

Řešení

Obslužná třída

Psát kód pro formulář ve třídách není o mnoho náročnější, než psát ho přímo na formuláři. Musíte jen vytvořit novou třídu (dle konvence se třída jmenuje jako formulář + přípona Form, např. SalesTableForm), vytvořit na formuláři její instanci a předat do ní reference objektů, které budete potřebovat. Pak už je vše podobné jako obvykle, jen místo formulářových metod vytváříte metody v nové třídě.

Pokud pracujete ve verzích nižších než 2012 a vaše řešení může být rozšiřováno ve vyšších vrstvách (např. koncovým uživatelem v USR vrstvě), doporučuji vytvořit takovou třídu jakmile potřebujete přidat na formulář jakýkoli kód. V ostatních případech můžete začít s kódem na formuláři (pokud ho nepotřebujete volat z jiných objektů apod.), ale musíte být připraveni ho zrefaktorovat do třídy, jakmile se začne komplikovat.

Obslužná třída je úzce svázána s formulářem a proto musí běžet na klientu. Kód, který má běžet na serveru, implementujte v jiných třídách, případně v serverových metodách.

Překvapivě často programátoři ignorují i již  existující třídy a píší kód přímo do formulářů. Zde musí nastoupit kontrola kvality a vzdělávání vývojářů.

Refaktorování

Refaktorování formulářů s množstvím kódu je obvykle dost namáhavé, ale je to většinou velmi dobrá investice (ve srovnání s dlouhodobými problémy s udržovatelností, výkonem apod.) Hlavní problém je, že se zpravidla dělá až když složitost logiky na formuláři přeroste to, co je vývojový tým schopen udržovat.

V první fázi udělejte obslužnou třídu a přesuňte kód do ní bez velké snahy o změnu struktury. Tohle je nejobtížnější fáze, protože musíte řešit všechny odkazy na automaticky deklarované proměnné (ovládací prvky, datové zdroje), netušíte, které metody jsou volány jinými objekty, nemáte dostatečnou podporu křížových referencí atd.

Jakmile máte kód ve třídě, nastavte co nejrestriktivnější přístup k metodám (private apod.) – to vám ukáže, co můžete bezpečně zrefaktorovat a kde musíte brát ohled na další objekty. Druhá fáze refaktorování je o rozdělení kódu do více tříd dle jejich zodpovědnosti, potřeb (např. běh na serveru), využití dědičnosti a podobně.