Oricine a lucrat deja cu WPF, este sigur că este familiarizat cu modelul MVVM (a făcut referințe la sfârșitul articolului). La urma urmei, conceptul MVVM este simplu și, cel puțin intuitiv, beneficiul utilizării sale ar trebui să fie ușor de înțeles. Dacă doriți să se în toată splendoarea sa se manifeste, este cât mai mic posibil pentru a pune logica în «Cod spatele» utilizator de control (UserControl) și, în orice caz, nu folosiți o legătură directă la interfața de utilizare în cadrul ViewModel'ey. Această abordare vă va oferi un profit mare sub forma testării ViewModel'e separat de controale. O altă bună practică va fi aceea de a minimiza crearea de instanțe ale ViewModel direct în controale. Este intolerabil, dacă controlul în sine creează un anumit tip de ViewModel - în acest caz, va fi mai dificil să puneți orice păpușă de test sub control. În caz contrar, vor exista lucruri au fost atunci când un control parental este ocupat crearea ViewModel'ey pentru alte ecrane, pentru că atunci codul poate deveni un non-test o grămadă de spaghete. Dacă alte ViewModels sunt responsabile de crearea ViewModel, atunci testarea va deveni mult mai ușoară.
Să ne imaginăm o aplicație cu o bară de navigare, mai multe ecrane și casete de dialog. Ceva similar este prezentat mai jos.
Putem vedea mai multe entități: fereastra principală, bara de navigare cu butoane, pagina curentă și dialogul de deasupra acestei pagini. În aplicația noastră pentru navigarea pe pagini, ați putea utiliza HyperLink, în loc să utilizați butoanele TextBlock cu HyperLink ca conținut. HyperLink are o proprietate care specifică numele de cadru în care să sară la o pagină nouă. Și ca totul este bine, dar folosind HyperLink pare dificil de a transfera pagina la ViewModel dorit.
Am văzut în rețea câteva soluții la această problemă:
- În cadrul evenimentului Frame.Navigated din fereastra principală a aplicației, prin Code Behind, puteți accesa conținutul încărcat în cadru și plasați-l acolo în Cod Behind ViewModel. Astfel, crearea ViewModel pentru toate paginile va fi concentrată într-un singur manipulator folosind o amprentă lungă dacă ... altfel dacă ... sau comutați. Despre faptul că testarea unui astfel de proces de navigare "greu codificat" este extrem de dificil de automatizat, am tăcut.
- O altă soluție este de a crea o instanță Page ViewModel'i și sub ea, în podkladyvanie ViewModel'i Pagina DataContext instanță și apel Navigați în cadru cu transmiterea exemplu Pagină creată. Această soluție este puțin mai bună decât cea precedentă, dar nu este încă "calea MVVM".
- A treia soluție este utilizarea bibliotecilor PRISM. Se utilizează în sectorul aplicațiilor de întreprinderi mari pentru implementarea interfeței UI compuse. Dacă sunteți familiarizat cu AngularJS, veți înțelege ce este. Este implementat un anumit RegionManager, în care sunt înregistrate părți ale UI. Apoi, prin managerul creat, controlul este instanțiat pe baza unui pseudonim care nu este atribuit, precum și alocarea contextului de date necesar. Această funcționalitate este similară cu cea implementată deja în WPF-ul NavigationService.
Primele două soluții sunt o cârjă evidentă. PRISM este un cadru de compoziție UI întreg. Investiția în studiul său, bineînțeles, merită, dar pentru aplicații mici (dovada conceptului, de exemplu), utilizarea unor lucruri precum IoC și PRISM ar putea să nu fie potrivite.
Cea mai simplă soluție ar putea intra mai mult sau mai puțin în contextul MVVM? Clasa Page din Silverlight are o metodă OnNavigatedTo supraîncărcată. În această metodă, ar fi convenabil să acceptați ViewModelul trecut la NavigationService.Navigate (Uri uri, object navigationContext) ca al doilea parametru. Cu toate acestea, în WPF nu există o astfel de metodă. Cel puțin nu l-am găsit sau ceva echivalent. Avem nevoie de un intermediar sau, dacă doriți, de un manager care să monitorizeze tranzițiile de pagină și să mute ViewModel-ul necesar de la parametrul metodei la DataContext. Implementarea unui astfel de manager de navigație va fi discutată în acest articol.
În următoarea secțiune, voi vorbi despre implementarea kernelului de soluție, despre managerul de navigație. Apoi, se va spune ce să implementați pe straturile UI și ViewModel. Pentru a economisi timp, puteți citi secțiunea "Manager de navigare" și gândiți-vă la restul în rezolvarea problemelor.
Cui îi pasă să se uite la codul deodată, poate merge la depozitul de pe GitHub.
Manager de navigare
Acest manager este implementat sub forma unui singleton cu verificare dublă a instanței la null (așa-numita versiune Double-Check Locking Singleton, versiunea Multithreaded a singleton). Folosirea unui singleton este preferința mea. Deci este mai ușor pentru mine să controlez ciclul de viață. Ai putea avea o clasă statică simplă.
Codul de implementare pentru singleton, vezi mai jos.
În codul de mai sus, puteți vedea că am făcut proprietatea instanței privată. Acest lucru se face pentru simplitate, astfel încât nimic nu ar fi inutil. În practică, este posibil să aveți nevoie să îl faceți public. În loc de proprietatea privată a instanței singleton, am creat o proprietate publică a serviciului de navigație Service (de tip NavigationService), care traduce apelurile printr-o instanță privată singleton. A fost posibil să se facă contrariul, dar atunci toate apelurile din exterior ar trebui să se facă prin instanță, adică
Alegeți opțiunea care vă place cel mai bine. Cred că această din urmă opțiune este mai simplă, dar necesită o implementare suplimentară a proprietăților și metodelor statice. Prin urmare, prin implementarea noii funcționalități ar putea fi mai profitabil să se deschidă proprietatea instanței (Navigation.Instance).
Proprietatea Serviciu în acest singleton va stoca o referință la NavigationService din instanța Frame în care doriți să efectuați tranziții de pagină. Puteți atribui valoarea curentă acestui link la începutul aplicației (în modulul de preluare a evenimentului încărcat din fereastra principală) sau la orice alt moment ulterior înainte de a apela una dintre metodele de navigare.
În exemplul de mai sus, atribuim fereastra de navigare principală în cadrul nostru de NavigareService. În loc de fereastra principală ar putea exista orice control, dar trebuie să luați NavigationService în evenimentul Loaded al acestui control. Înainte de acest eveniment, puteți obține nul. În detaliu, ciclul de viață al controalelor și NavigationService nu am studiat.
Ca scenariu alternativ, aș putea sugera utilizarea ChildWindow din WPF Toolkit Extended. în care este încorporat un alt cadru. În acest caz, este posibil să înlocuiți temporar NavigationService în navigatorul nostru pentru a face o tranziție în interiorul unui astfel de dialog. Aceasta se va automatiza prin încărcarea diferitelor ecrane în casetele de dialog. Dar scenariul unei astfel de utilizări pare foarte exotic, prin urmare nu voi detalia detaliile. Dacă un astfel de scenariu este interesant, atunci voi scrie un articol separat.
Serviciul de proprietate de navigare
Într-un mod bun, în setter (și metodele publice ale managerului de asemenea) nu este suficient de utilizat de blocare. Dar, în general, dacă aveți în aplicație în paralel cu apelul oricărei metode de navigare, NavigationService va fi înlocuit. atunci, cel mai probabil, ceva este implementat incorect. În timp ce pentru simplitate, vom face fără blocare. dar v-am avertizat.
Următoarele sunt metodele publice de navigare.
În codul de mai sus, puteți observa utilizarea "_resolver". În secțiunea IoC voi spune despre el. Pe scurt, aceasta este cea mai simplă implementare a Containerului pentru Inversiunea Controlului.
În managerul de navigare, un subset de metode de navigare este implementat de la NavigationService. care este suficientă pentru majoritatea cazurilor simple. Rămâne doar să punem ViewModelul transmis în proprietatea DataContext a paginii de destinație. Acest lucru se face în modulul de gestionare a evenimentelor navigate (a se vedea codul de mai jos).
Manipularea evenimentelor Navigat
În manualul evenimentului Navigated, se face o încercare de a aduce conținutul Frame-ului la tipul de pagină. Astfel, vor fi prelucrate numai tranzițiile către Pagină. Toate celelalte sunt filtrate. Dacă doriți, puteți elimina această "Cortină de fier". În cazul unei distribuții reușite, instanța modelului ViewModel trecut în proprietatea ExtraData a evenimentului eveniment va fi plasată în DataContext a paginii de destinație. Este vorba de managerul de navigație.
Rămâne să creați un ansamblu cu implementarea paginilor și a modelului ViewModel'e. Am implementat, de asemenea, constructorii Helpers în care am plasat codul de implementare RelayCommand pentru ViewModel. Dacă există forțe și timp, mergeți la secțiunile următoare cu o descriere a implementării UI și ViewModel. Dacă nu, voi rezuma pe scurt ce altceva să pună în aplicare.
Pentru fiecare pagină, creați un ViewModel separat. Aceste modele de vizualizare "private" sunt instanțiate în mama lor MainViewModel folosind "Inversion of Control" (vezi secțiunea IoC). ViewModelul principal este plasat în DataContext al ferestrei principale, dar cu același succes ar putea fi instanțiat ca resursă statică XAML în dicționarul de resurse al ferestrei principale sau chiar la nivelul întregii aplicații. În acest caz, în legăturile DataContext, va trebui să specificați ceva de genul Source =. Dar nu vă puteți îngrijora dacă DataContext-ul părintelui logic este moștenit în locul potrivit.
În MainViewModel, am creat mai multe comenzi. Unul pentru a merge la aliasul de șir al paginii specificat în CommandParameter (tranziție fără a transfera contextul de date). Alte comenzi conțin un delegat Execute în delegația lor pentru a naviga printr-un alias specific al paginii de destinație, care este primit prin intermediul CommandParameter din contextul de date. Pentru detalii, puteți merge la GitHub sau continuați să citiți acest articol.
Construiți ViewModels
Această ansamblu furnizează un model de bază ViewModel care implementează INotifyPropertyChanged.
Restul modelelor ViewModels sunt moștenite de la el și în prezent sunt extrem de simple. Acestea conțin o proprietate unică de șir cu un nume unic (a se vedea exemplul de mai jos).
Rețineți că aici proprietatea este numai pentru citire și nu numește RaisePropertyChanged (...) oriunde. În acest caz, acest lucru a fost făcut pentru simplitate. În practică, astfel de proprietăți ale modelului ViewModel sunt întâlnite, dar rareori, pentru că Legarea acestor proprietăți va funcționa o singură dată. Chiar dacă adaug un setter fără RaisePropertyChanged (...), obligația va fi încă "o singură dată".
MainViewModel este deja mult mai complicat. După cum am scris pe scurt în secțiunea anterioară, acesta va stoca ViewModels private și va implementa comenzile de navigare. În cazul meu, ViewModels private sunt create o singură dată folosind așa-numitul "Resolver" a ", atunci când inițializează MainViewModel. Prin urmare, am pus în aplicare numai getters de aceste ViewModels.
Câmpurile sunt inițializate în constructorul MainViewModel:
"_resolver" în acest caz este un alt Container de Inversiune de Control, care va fi discutat în secțiunea corespunzătoare. În acest moment, acest Resolver extrage pur și simplu un delegat din dicționar, corespunzător aliasului ViewModel. De asemenea, merită menționat faptul că, în practică, este posibil să fie necesară implementarea completă a câmpurilor și a proprietăților pentru ViewModels privat. Acest lucru este deja făcut elementar.
Comenzile din cazul meu sunt implementate cu indicația de a obține și a seta. iar inițializarea instanțelor acestora este plasată într-o funcție separată. Prezența setrilor de comandă mi-a permis să înlocuiesc fiecare comandă în afara modelului ViewModel curent. Această abordare permite, de exemplu, schimbarea răspunsului casetei de dialog la apăsarea butonului "OK" dacă este legată prin legare la comanda corespunzătoare din modelul său (intern) ViewModel. Cu toate acestea, acest scenariu este foarte exotic și poate fi implementat fără echipe de stabilire.
Rețineți că aliasul paginii de destinație este trecut ca cale. Am pus aceste pseudonime sub formă de constante în managerul de navigare, dar, în general, cel mai bun loc pentru ele în fișierul de configurare XML sau doar într-un dicționar de text.
Fereastra principală și asamblarea Pagini
Pentru comoditate, vă voi da o privire la aplicația de testare.
În stânga sunt patru butoane. Primul buton este legat de comanda GoToPathCommand și efectuează o tranziție la Page1 fără contextul de date. După accesarea paginii fără contextul de date, în loc de valoarea curentă din ViewModel, valoarea din parametrul FallbackValue al obiectului Obligație va fi înlocuită. Restul butoanelor sunt legate de comenzile "private" cu aliasul paginii de pagini dorite specificat în delegatul echipei.
Aspectul și codul ferestrei principale
Ansamblul Pagini conține patru pagini: Page1, Page2, Page3, Page404. Primele două conțin doar blocuri de text care sunt legate de proprietatea ViewModel-ului privat corespunzător. Al treilea a fost un pic complicat pentru mine să pun în aplicare o altă problemă MVVM, și anume sarcina de a obliga ListBox.SelectedItems la ViewModel. Acesta este un subiect separat, care, în opinia mea, merită un articol separat. Pentru interes, poți să privești marcajele de spoiler de mai jos.
IoC (Inversiunea Managementului)
Din păcate, nu pot descrie în detaliu abordarea în acest articol. Volumul și atât de mare. Dar puteți învăța cunoștințele necesare, de exemplu, a articolelor de pe Habré. De asemenea, o multime de resurse pe care le puteți Google. Pe scurt, „inversiunea de control“ este o modalitate de a elimina link-urile directe într-un ansamblu la altul de asamblare. Dependență Injectarea se face prin „container“ special care dintre fișierele de configurare ce recunosc clase specifice și adunărilor publice pentru a inițializa interfața și indică numele secțiunii în fișierul de configurare. Este necesar să se admită că, în codul meu IoC nu este pusă în aplicare pe deplin. Pentru a fi sincer, obiectiv, iar acest lucru nu a fost. Desigur, conceptul de IoC în codul am încercat să reflecteze și a încercat să arate cum să facă codul de mai puțin conectat.
Mai jos sunt interfețele containerelor și implementarea acestora.
Aceste interfețe joacă rolul unor contracte pentru diferite implementări ale containerelor de pagină și ViewModel. În momentul de față am făcut două implementări, pe care nu ar trebui să le folosiți în proiecte reale.
Implementarea containerelor de testare
Acestea sunt doar "păpuși" ale containerelor care trebuie înlocuite cu ceva care poate fi controlat, de exemplu, de la biblioteca Unity. Ca interfețe, ar fi mai bine să folosiți ceva de genul IUnityContainer. dar nu am vrut să ponderez soluția cu referință suplimentară și să complici percepția implementării navigatorului. În plus, puteți prefera orice altă bibliotecă IoC în loc de unitate.