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

0 Comments
LINQPad Jedna od glavnih značajki Silverlight i WPF aplikacija je implementacija INotifyPropertyChanged interfacea, pomoću kojega se obavještava da je promijenjena vrijednost nekog property-a. Jedan tipični primjer "pretplatnika" na takve obavijesti je sučelje aplikacije; ukoliko se deklarativno u XAMLu poveže klasa, odnosno njen property za neki UI element, prilikom promjene vrijednosti propertya unutar te klase sučelje će automatski ispisati promijenjene vrijednosti. U propertyu klase kod set operacije moramo podignuti event i obavijestiti sve zainteresirane da je došlo do promjene :
public string PropertyName
{
    get { return _propertyName;  }
    set { 
        _propertyName=value; 
        NotifyPropertyChanged("PropertyName"); 
    }
}
Metoda NotifyPropertyChanged i parametar u obliku stringa je standardni način obavještavanja o promjeni vrijednosti. Takav način upotrebe "Magic stringova" je podložan bugovima, jer zamislimo da promijenimo naziv samog property-a ali ne i string unutar poziva metode NotifyPropertyChanged. Kompajler bi uredno obavio svoj posao bez dojave greške, a aplikacija bi se počela čudno ponašati. Baš zbog toga ja želim ovako pisati podizanje eventa:
public string PropertyName
{
    get { return _propertyName;  }
    set { 
        _propertyName=value; 
        this.RaisePropertyChanged(x=>x.PropertyName); 
    }
}
Ovo je c# 3.0 izraz (Expression), s kojim dobijemo potpuni Intellisense i compile time provjeru. Metoda RaisePropertyChanged je sada izvedena kao extension metoda. Cijeli kod ovako izgleda:
public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

public static class ViewModelExtension
{
    public static void RaisePropertyChanged<T , TProperty>(this T observableBase, Expression<Func<T, TProperty>> expression) where T : ViewModelBase
    {
        observableBase.RaisePropertyChanged( observableBase.GetPropertyName(expression) );
    }

    public static string GetPropertyName<T , TProperty>(this T owner, Expression<Func<T, TProperty>> expression)
    {
        var memberExpression = expression.Body as MemberExpression;
        if (memberExpression == null)
        {
            var unaryExpression = expression.Body as UnaryExpression;
            if (unaryExpression != null)
            {
                memberExpression = unaryExpression.Operand as MemberExpression;
                if (memberExpression == null)
                    throw new NotImplementedException();
            }
            else
                throw new NotImplementedException();
        }
        var propertyName = memberExpression.Member.Name;
        return propertyName;
    }
}
ViewModelBase je apstraktna klasa koju mora nasljeđivati klasa koja je bindana na sučelje. Ovaj kod sam preuzeo iz Silverlight MVVM toolkita, jer je puno ljepši od moje implementacije :) Ali glavna značajka je da se zasniva na izrazu Func<T, TProperty>, preko kojega se dođe do tijela izraza, odnosno naziva propertya (expression.Body, castano u MemberExpression, i onda Member.Name metoda tog MemberExpression-a, koja pomoću refleksija čita sam naziv propertya!).

Ovaj kod se može upotrijebiti u bilo kojem projektu gdje se mora čitati naziv propertya, samo se promjeni uvjet where T: ViewModelBase u neki drugi baznu klasu, ili pak makne cijeli taj uvjet!