9 Comments
MVVM Silverlight WPF

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!?

3 Comments

U izradi sučelja jedne ASP.NET MVC aplikacije morao sam dobiti listu radio buttona, sa jednim defaultnim odabirom. Jednostavno, pomislih, postoji HtmlHelper za to. Pogledah po spisku u Intellisense autocomplete dialogu, i stvarno je tamo, Html.RadioButtonList(). Tu je moj sreća završila, jer taj helper ništa ne radi, odnosno ja nisam uspio dokučiti što da radim sa njime! Dobijem array stringova, sa zapisima input type="radio" /> , koje bi ja trebao sa foraech petljom ispisivati, plus dodavati labele jer njih helper nije napravio. Iz vjerskih uvjerenja nisam htio odrađivati posao helpera, pa mi nije ostali ništa drugo nego da napravim svoj. I konačno malo prilike da se poigram sa Reflectionom, tim mracnim djelom frameworka kojim me baka plašila kada sam bio mali ;)
Cilj je dobiti ovakav kod:




Html Helper kao extension metoda na HtmlHelper klasu:

namespace Microsoft.Web.Mvc
{
	public static class HelperExtMethods
	{
		public static string MyRadioButtonList(this HtmlHelper helper,string name, SelectList list)
		{
			PropertyInfo DataInfo = null;
			PropertyInfo ValueInfo = null;
			StringBuilder builder=new StringBuilder();
			string pattern = "";
			foreach (var item in list.Items)
			{
                if (DataInfo == null)
				{
                    DataInfo = item.GetType().GetProperty(list.DataTextField);
					ValueInfo = item.GetType().GetProperty(list.DataValueField);
				}
                string data = DataInfo.GetValue(item, null).ToString();
				string value = ValueInfo.GetValue(item, null).ToString();
				if(list.SelectedValue==value)
					builder.AppendFormat(pattern, name, value, "radio" + value, data,"checked");
				else
					builder.AppendFormat(pattern, name, value, "radio" + value, data,"");
			}
			return builder.ToString();
		}
	}
}

Glupo ime helper, ali poslužiti će dokle ne smislim nešto bolje (RadioButtonListTurboDiesel npr?).
Upotreba helpera:

<%= Html.MyRadioButtonList("GodisnjeDoba",new SelectList(Model.ListaDoba,"Id","Naziv",Model.SelectedDoba)) %>
Doba (u Model.Doba) je klasa koja ima dva propertya, Id i Naziv. Pomoću refleksija u foreach petlji čitamo njihov sadržaj i kreiramo potrebni Html. Iako su refleksije najsporiji način dohvata vrijednosti u objektima, mislim da za ovih nekoliko objekata koje čitamo preko refleksija ne utječemo previše na vrijeme učitavanja stranice. Ako znate bolji (brži) način čitanja propertya, ostavite komentar pa ću prepraviti ovaj kod i dati drugu verziju!

Oprez: ovaj kod ne sadrži nikakve provjere valjanosti ulaznih parametra; što znači da ako mu ne pošaljete na primjer defaultnu vrijednost za selektirani radio button, dobiti će te za nagradu asp.net-ov yellow screen of death. Upozoreni ste!