Implementacija MVVM patterna u Silverlight aplikaciji
Silverlight sa svojim codebehind konceptom je pogodan, štoviše i preporučen, za implementaciju nekog od patterna za separaciju poslovne logike i logike za pristup podacima. Od njegovog izlaska, zbog bliskosti sa WPF modelom, programeri su preuzeli MVVM (Model View ViewModel) pattern (obrazac?) koji je popularan u WPF svijetu, sa određenim preinakama. Uz MVC (Model View Controller) i MVP (Model View Presenter) patterne koji su svoju populatnost stekli u web aplikacijama, odnosno stateless okruženju, MVVM je posebno pogodan za Silverlight/WPF sučelja jer koristi njihovu bogatu mogućnost Data Bindinga, odnosno deklarativnog povezivanja objekata u XAML kodu.
MVVM se sastoji od tri komponente:
1. View: XAML sa svojim code behindeom. U XAMLu deklarativno povežemo propertye UI elemenata, a u code behindeu zbog nemogućnosti nativnog povezivanja za UI evente (Command Binding u WPFu), pozivamo metodu od ViewModela. Ovo se može zaobići dodavanje MVVM toolkit (verzija sa codeplexa i od GalaSofta) ili SilverlightFX frameworka u projekt.
2. ViewModel: klasa koja sadrži podatke potrebne za renderiranje sučelja, kao i metode za operacije nad modelom. Da bi sučelje reagiralo na promjeno stanja određenih propertya, potrebno je da ViewModel klasa implementira INotifyPropertyChanged interface. U prijevodu, kada mi promijenimo vrijednost propertya u ViewModelu, želimo da se sučelje automatski osvježi sa novom vrijednosti. Povezivanje mora biti dvosmjerno, što znači da promjenom podataka na sučelju se mijenja i sadržaj u ViewModel objektu – potrebno za spremanje podataka na server/bazu/gdjegod
3. Model: objektni model podataka naše domene. Povezivanjem na WCF servis (ili “stari” ASMX web servis), Visual Studio generira proxy klase za komunikaciju, i potpunu presliku klasa koje web servis daje. Ako smo izradili LINQ2SQL ili Entity Framework klase, te iste klase će biti dostupne i u Silverlightu!
Krenimo u izradu jednostavne Silverlight MVVM aplikacije. Zadatak je prikazati formu za editiranje podataka. [more] Aplikacija će sadržavati ove klase:
Model klasa Person:
public class Person { public string Ime { get; set; } public string Prezime { get; set; } }
Napomena: obično i model klase implementiraju INotifyPropertyChanged interface, da bi se preko ViewModel klase promjene dojavile i View-u (naravno ako ViewModel ima property tipa Person)!
ModelCatalog : klasa za dohvat podataka sa servera koju ViewModel koristi preko interfacea. Pošto se radi o asinkronom dohvatu, ViewModel se mora pretplatiti na event koji će biti podignut kada podaci stignu sa servera.
public interface IModelCatalog { void GetPerson(); void SavePerson(Person person); event EventHandler<PersonEventArgs> PersonLoadCompleted; }
ViewModel pozove GetPerson() i pretplati se na PersonLoadCompleted event, te primi učitane podatke preko PersonEventArgs argumenta:
public class PersonEventArgs : EventArgs { public Person Data { get; private set; } public PersonEventArgs(Person result) { Data=result; } }
da ne ulazimo u izradu web servisa i njegovo povezivanje sa Silverlightom, napraviti ćemo jedan “lažni” katalog podataka koji simulira komunikaciju sa web serverom:
public class FakeCatalog : IModelCatalog { public void GetPerson() { // tu ide asinkroni poziv web servisa var person=new Person() { Ime="John", Prezime="Skeet" }; var argument=new PersonEventArgs(person); // kada servis vrati podatke, dojavimo pretplaćenim metodama: if(PersonLoadCompleted!=null) PersonLoadCompleted(this,argument); } public void SavePerson(Person person) { // implementacija poziva web servisa i spremanje podataka } public event EventHandler<PersonEventArgs> PersonLoadCompleted; }
ViewModel: klasa koju ćemo bindati za View, stoga mora sadržavati sve potrebne podatke za prikaz sučelja. U svom konstruktoru prima implementaciju IModelCatalog i poziva metodu za učitavanje. Također, sadrži implementaciju INotifyPropertyChanged interfacea.
Napomena: za potrebe ovog primjera, napraviti ćemo da se objekt Person, koji se dobije iz IModelCatalog-a, mapira na nekoliko propertya ViewModel klase.
public class MainViewModel : INotifyPropertyChanged { private IModelCatalog modelCatalog; public MainViewModel(IModelCatalog mc) { modelCatalog=mc; modelCatalog.PersonLoadCompleted += (sender,args)=> { this.FirstName = args.Data.Ime; this.LastName = args.Data.Prezime; } modelCatalog.GetPerson(); } private string _name; public string FirstName { get { return _name; } set { _name=value; NotifyChange("FirstName"); } } private string _lastname; public string LastName { get { return _lastname; } set { _name=value; NotifyChange("LastName"); } } public void Save(object sender,EventArgs e) { var person=new Person() { Ime=this.FirstName, Prezime=this.LastName }; modelCatalog.Save(person); } // Implementacija INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; public void NotifyChange(string propertyName) { if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
Naš ViewModel sada implementira propertye koje ćemo bindat na sučelje, kao i metode koje ćemo povezati s eventima od UI elemenata sučelja.
View (MainPage.xaml.cs): za povezivanje ViewModela za sučelje koristimo DataContext svojstvo od parcijalne codebehind klase.
Osim za povezivanje našeg ViewModela za View (XAML) i osnovne interakcije između View elemenata (prepisivanje propertya Button i slično), u codebehindeu se ne smije nalaziti nikakav kod koji uključuje poslovnu logiku, logiku pristupa podacima, manipulacije nad podacima i slično!
public partial class MainPage : UserControl { private MainViewModel viewModel; public MainPage() { InitializeComponent(); this.Load += MainPage_Loaded; } public void MainPage_Loaded(object sender, RoutedEventArgs e) { // injektiranje IModelCatalog objekta može ići preko nekog IoC/DI frameworka ili posebnog “locator” objekta! viewModel=new MainViewModel(new FakeCatalog()); // povezivanje XAML UIa sa našim objektom this.DataContext = viewModel; SaveButton.Click += viewModel.Save; } }
U View-u (MainPage.xaml), odnosno XAMLu, bindamo UI elemente za propertye odnašeg ViewModela (primjer djelomičnog koda, bez definiranja UserControl elementa, xml namespace definicija) :
<TextBox Text="{Binding FirstName, Mode=TwoWay}" /> <TextBox Text="{Binding LastName, Mode=TwoWay}" /> <Button x:Name="SaveButton" Content="Spremi"/>
ovo je naravno primjer XAMLa bez stilova, tako da ga svakako treba doraditi da izgleda imalo upotrebljivo. Bitno je da prilikom bindanja navedemo da dvosmjerni tip (Mode=TwoWay).
Ovime smo završili implementaciju MVVM patterna u našu malu Silverligh aplikaciju, i s time odvojili kod po ulogama i omogućili lakše održavanje i testiranje. U skorije vrijeme se nadam da ću dodati i automatsko bindanje metoda iz ViewModela za View evente (Button click), čime bi mogli izbjeći cijeli codebehind! Nadam se uskoro, čim pronađem najjednostavniji način bez upotrebe trećih frameworka. Ili naravno, vi predložite implementaciju tog featura!?