Pe masura ce dimensiunea unui proiect creste, nu de putine ori apare senzatia ca acesta incepe sa alunece si este din ce in ce mai greu de mentinut, senzatia ca fiecare modificare introduce regresii in alte parti, sau pur si simplu anumite parti nu mai functioneaza deloc. Acest lucru este cauzat de cele mai multe ori de un code-design defectuos. In cadrul aplicatiilor mari, unul din pricipalele deziderate pe care le vizeaza arhitectura este decuplarea cat mai buna a interfetei grafice de logica. Exista cateva motive functionalitatea logica este indicat sa fie pastrata separat de interfata:
- Posibilitatea refolosirii aceleiasi logici din parti diferite ale UI-ului (de exemplu, am dori sa putem salva documentul curent si din meniu, si din toolbar).
- Pentru a putea aplica Unit Tests pe partea de logica.
- O logica structurata bazata pe domain objects este mai usor de mentinut si de inteles decat una in care UI-ul este foarte tare cuplat cu logica.
- O problema clasica si des intalnita este cea a sincronizarii diverselor parti ale UI-ului. In momentul in care o actiune devine indisponibila (cum ar fi salvarea documentului, pentru ca tocmai a fost inchis), in mod normal trebuie sa dezactivam toate elementele din interfata care permit salvarea (butoane, elemente de meniu, toolbar etc). De cele mai multe ori o actiune genereaza una sau mai multe alte schimbari in starea interfetei, si posibilitatea sau imposibilitatea efectuarii altor actiuni, iar acele actiuni la randul lor pot fi apelate din multe locuri din UI, lucru care duce la cod duplicat, complicat si foarte multe batai de cap.
Pattern-ul Command
Aici intra in scena pattern-ul Command. In principiu, acesta presupune definirea aceleiasi comenzi care poate fi apelata din mai multe locuri. Pentru acest lucru, WPF pune la dispozitie interfata ICommand, cu doua metode:
- Executed – este hadlerul care se apeleaza la executia comenzii.
- CanExecute – este o metoda pe care WPF o apeleaza periodic pentru a detecta daca comanda poate fi apelata.
Mebrii de tip Button, MenuItem s.a.m.d au o proprietate Command, in care se poate pune o instanta de ICommand. Starea de enabled sau disabled a controlului este setata in functie de valoarea furnizata de metoda CanExecute a comenzii.
Se poate vedea ca logica se poate decupla total de partea de UI, iar in cazul in care folosim doar comenzi, code-behind-ul poate sa fie gol. Tot ce trebuie sa facem este sa facem binding la proprietatea Command a obiectului nostru de interfata, si starea UI-ul este automat setata de WPF prin polling-ul metodei CanExecute. Nu este necesar sa tratam evenimente de genul Button_Clicked.
Comenzile actioneaza direct asupra business logic-ului, si nu a interfetei. Este important ca ele sa nu contina o referinta catre UI, pentru ca in acest caz nu beneficiem de decuplarea UI-ului de logica. UI-ul contine o referinta catre comenzi, dar nu si viceversa.
Pattern-ul Routed
In partile precedente, a fost explicata modalitatea in care functioneaza evenimentele rutate. RoutedCommand implementeaza ICommand si functioneaza intr-un mod similar, si anume prin propagarea pe arborele vizual, catre radacina. Este deci destul sa avem un singur handler la baza arborelui pentru comenda noastra. Acel handler va fi apelat, indiferent de unde este initiata, chiar daca e vorba de fisiere .xaml diferite. Practic putem avea mai multe UserControls imbricate unul in altul, fara sa fie necesara inregistrarea handlerelor pentru RoutedCommand-ul nostru in fiecare. Este pur si simplu de ajuns ca handlerele sa fie inregistrate in fereastra care contine aceste UC-uri, iar comanda se va propaga catre radacina, si va fi executat handlerul de acolo.
Spre deosebire de evenimentele routed, in cazul comenzilor doar primul handler gasit este luat in considerare, atat pentru Executed si cat pentru CanExecute. Dupa ce a fost gasit si rulat, comanda nu se propaga mai departe.
Atentie! Un alt lucru de care trebuie tinut cont este ca momentan Silverlight nu suporta RoutedCommands, doar interfata ICommand.
DelegateCommand
Exista cazuri in care nu dorim ca sa folosim propagarea comenzii prin arborele visual, pentru ca acest lucru presupune:
- Un cost de performanta.
- Probleme de mentinere a codului (daca UI-ul este foarte complicat si contine multe user controale imbricate, unui programator poate sa-i fie greu de urmarit exact unde se face handling-ul comenzii daca nu este familiar cu codul).
- RoutedCommands nu se pot folosi in Silverlight, deci nu putem porta o aplicatie din WPF in Silverlight.
In acest caz putem sa ne creem o clasa proprie care implementeaza ICommand, si sa furnizam instante de metode (delegates) pentru Execute si CanExecute. In acest caz putem fi siguri care sunt metodele care se apeleaza. Daca nu inregistram handleri in contextul actual al controlului, atunci nu se va rula nici un cod. Totul se intampla local, si este foarte usor de urmarit.