Καλώς ορίσατε στο dotNETZone.gr - Σύνδεση | Εγγραφή | Βοήθεια

[Μαθαίνοντας Design Patterns] Model – View – ViewModel

Συνεχίζοντας τη σειρά με τα design patterns, σε αυτό το post γίνεται μία αναφορά στο MVVM, μέσα από μία αρκετά απλοϊκή προσέγγιση. Θα περιγραφεί το pattern, θα δωθούν μερικές base classes, ένα utility για το πώς γίνεται type-safely raise ένα event για ανανέωση UI, καθώς και ένα μικρό παράδειγμα, με Bing Maps σε μία Silverlight εφαρμογή.

Τί είναι το MVVM;

Το Model – View – ViewModel είναι ένα πρότυπο σχεδίασης για το σχεδιασμό διεπαφών χρήστη, το οποίο έχει επηρροές τόσο από το Model View Presenter, όσο και από το Model View Controller. Είναι γνωστό  και ως Presentation Model (σχεδόν ίδιο)(PM στο εξής), όπως το έχει καταγράψει ο Martin Fowler. To Model – View – ViewModel (MVVM στο εξής) είναι επίσης ένας καθιερωμένος τρόπος, για τη δημιουργία εφαρμογών που βασίζονται και σε XAML και επιλέχθηκε για την ευελιξία που παρέχει στον προγραμματιστή κατά την ανάπτυξη (decoupling View-Model). Το design pattern που περιγράφεται στη συνέχεια με τη βοήθεια του παραδείγματος σε silverlight, παρουσιάζεται έχοντας κατα νου το ισχυρό πλαίσιο databinding του silverlight (η αντιστοιχία στοιχείου οθόνης με κανονικό Property ενός object).

Ξεκινώντας την περιγραφή του MVVM, αναφέρουμε ότι παρέχει (όπως και το PM) ένα επίπεδο αφαίρεσης του View (της εικόνας δηλαδή που φαίνεται στον χρήστη / XAML) στο οποίο ορίζεται τόσο η κατάστασή του, όσο και η συμπεριφορά του. Έστω ότι έχουμε την οθόνη διαχείρισης καταστημάτων μίας εταιρίας πώλησης παπουτσιών. Υπάρχουν δύο εκδόσεις για την εν λόγω οθόνη, μία που παρουσιάζει όλα τα καταστήματα (datagrid με μία λίστα) και μία που ενεργοποιείται όταν αλλάζουμε ιδιότητες από κάποιο κατάστημα (αρκετά controls, όπως textboxes κτλ με τα οποία μπορούμε να καταχωρούμε πληροφορίες για ένα selected store από την προηγούμενη σελίδα). Οι δύο οθόνες λέγονται αντίστοιχα Master και Detail σελίδες. Έστω ότι έχουμε την Detail σελίδα να αναπτύξουμε. Στη συνέχεια περιγράφεται πώς το MVVM μπορεί να διαχωρίσει τις αρμοδιότητες, για διαφορετικές ενέργειες που λαμβάνουν χώρα πίσω από την οθόνη.

Τα τρία στοιχεία που περιγράφονται είναι τρία διαφορετικά αντικείμενα στη μνήμη και έχουν πεδία και μεθόδους. Έτσι λοιπόν έχουμε το View (τονίζω πάλι ότι αναφερόμαστε σε XAML αρχείο) το οποίο περιέχει διάφορα controls με τα οποία μπορεί να αλληλεπιδράσει ο χρήστης. Κάθε control που περιέχεται, μπορεί να εμφανίσει πληροφορία είτε σε μορφή κειμένου είτε σε κάτι πιο πολυμεσικό.

Έστω ότι έχουμε ένα πεδίο κειμένου (textboxes) και έναν επιλογέα με μία μοναδική επιλογή κάθε φορά (combobox). Τα δύο αυτά controls, δένουν τις τιμές τους με ιδιότητες (properties) του ViewModel μέσω μηχανισμού databinding. Προγραμματιστικά το databinding ανάμεσα στο View και στο ViewModel (που περιέχει όλη την πληροφορία που δείχνει/ανανεώνει/… το View) γίνεται πάρα πολύ εύκολα, μιας και θέτουμε το ViewModel αντικείμενο, ως το πλαίσιο δεδομένων (datacontext) του View (βλ. παράδειγμα). Αν κάποια τιμή, σε κάποιο property αλλάξει, τότε η ίδια ιδιότητα ενεργοποιεί ένα γεγονός (κάνει raise ένα event propertychanged) που υποδηλώνει ότι άλλαξε τιμή και έτσι ενημερώνει το View ότι άλλαξε η τιμή για το συγκριμένο user control (για παράδειγμα ένα textbox – πεδίο εισαγωγής κειμένου). Κάθε πληροφορία δηλαδή, που είναι databound με ένα control όταν αλλάξει, ενημερώνει το UI ότι άλλαξε και ότι θα πρέπει να ανανεωθεί. Για να μπορεί ένα αντικείμενο (το ViewModel στην περίπτωσή μας) να αλληλεπιδρά με αυτόν τον τρόπο θα πρέπει να υλοποιεί το INotifyPropertyChanged interface. Έτσι λοιπόν ανανεώνεται και η αντίστοιχη πληροφορία στον χρήστη. Σε αντίθετη περίπτωση, όταν ένας χρήστης πατήσει ένα κουμπί, μία εντολή (Commanding) θα εκτελεστεί στο ViewModel και θα πραγματοποιήσει όλες τις αλλαγές στα δεδομένα του μοντέλου (τα οποία περιέχονται στο ίδιο το αντικείμενο του ViewModel).

To Model είναι το αντικείμενο το οποίο καλείται από το ViewModel για λειτουργίες πρόσβασης στα δεδομένα. Στην περίπτωσή μας έχουμε δομικές μονάδες που περιγράφουν το πεδίο της εφαρμογής (customers, orders, κτλ). Αξίζει να κρατήσουμε τρία πράγματα: image

 

  • To View ΔΕΝ γνωρίζει το Model (γιατί δεν απαιτείται κάτι τέτοιο εννοιολογικά)
  • To ViewModel ΔΕΝ γνωρίζει τίποτα για View (εξαρτάται, ίσως είναι θέμα προθέσεων του προγραμματιστή)
  • To Model ΔΕΝ γνωρίζει τίποτα για View

Στις XAML εφαρμογές, το View είναι η οθόνη που σχεδιάζουμε στο Expression Blend (την εφαρμογή της Microsoft για σχεδιασμό σελίδων), το ViewModel αναπτύσσεται στο Visual Studio και το Model είναι στην ουσία οι κλάσεις που χρησιμοποιούμε για το πεδίο της εφαρμογής μας οι οποίες περιέχουν όλες τις ιδιότητες οι οποίες υπάρχουν στο αντικείμενο. Στις RIA εφαρμογές το κανονικό μοντέλο υπάρχει στον server και μία πιο μινιμαλιστική έκδοση στον client, για να μπορεί να διαχειρίζεται τα αντικείμενα. Για παράδειγμα στην σελίδα με την επεξεργασία ενός στιγμιότυπου της οντότητας Shoe (π.χ. για rowId = 5) το ViewModel έχει ως ένα από τα πεδία του, ένα αντικείμενο τύπου shoe, το οποίο δηλώνεται στο Model. To Model, έχει στον server την κανονική έκδοση της οντότητας Shoe, ορίζοντας πρόσθετες ιδιότητες αν είναι απαραίτητο (που δεν εμφανίζονται στον client).

Στις ακόλουθες ιεραρχίες μπορούμε να δούμε την κλάση ViewControl που είναι στην ουσία η οθόνη μας, που γνωρίζει το ViewModel και το έχει θέσει ως το Data Context της. Υπάρχει ένα κάπως well defined API μέσα από generic type constraints.

 

Base Classes

public class ViewControl : UserControl, IView {     private IViewModel viewModel;     public ViewControl()     {     }     protected ViewControl(IViewModel viewModel) : this()     {         SetViewModelInternal(viewModel);     }     public virtual TViewModel GetViewModel<TViewModel>()         where TViewModel : class, IViewModel     {         return this.viewModel as TViewModel;     }     public virtual void SetViewModel(IViewModel model)     {         this.SetViewModelInternal(model);     }     private void SetViewModelInternal(IViewModel model)     {         this.viewModel = model;         this.DataContext = model;         PropertyChanged.Raise(() => DataContext);     }     #region INotifyPropertyChanged Members     public event PropertyChangedEventHandler PropertyChanged;     #endregion } public interface IView : INotifyPropertyChanged { } public interface IView<TViewModel> : IView     where TViewModel : IViewModel {     TViewModel ViewModel { get; set; } } public interface IViewModel : INotifyPropertyChanged { } public interface IViewModel<TView> : IViewModel     where TView : IView {     TView View { get; set; } } public abstract class ViewModel<TView>     where TView : IView {     public bool IsDesignTime     {         get         {             return (Application.Current == null) || (Application.Current.GetType() == typeof(Application));         }     }     #region INotifyPropertyChanged Members     public event PropertyChangedEventHandler PropertyChanged;     #endregion }

Έστω η περίπτωση που έχω controls που στέλνουν εντολές, buttons, κλπ κλπ. Θα πρέπει να υπάρχει μία δομή στο XAML, που θα δηλώνει το πια εντολή είναι αυτή που θα πρέπει να εκτελεστεί όταν πατηθεί το κουμπί. Στην περίπτωση του code behind μπορούμε να παίξουμε με handlers αλλά με MVVM μπορώ να έχω μία διαφορετική προσέγγιση. Γιατί να μη εκμεταλλευτώ το μοντέλο του MVVM και να “βρωμίσω” το code behind μου με κώδικα hard-wired με το View? Έτσι λοιπόν ένα σημείο κλειδί είναι να χρησιμοποιήσουμε Commands. Στο WPF υπάρχουν πραγματικά commands που σε συνδυασμό με τα routed events δημιουργούν την έννοια του routed command, αλλά δεν θα μιλήσουμε εδώ γι’ αυτό. Στο Silverlight (3) υπάρχει το ICommand interface το οποίο είναι στην ουσία το εξής, και θα αναλύσουμε σε κάποια άλλη συζήτηση για το commanding σε MVVM:

interface ICommand {     void Execute(object parameter);     bool CanExecute(object parameter);     event EventHandler CanExecuteChanged; }

 

Bing Maps MVVM

Στο συγκεκριμένο παράδειγμα παραθέτω το ViewModel μίας οθόνης που έχει ένα Bing Maps control. Θέλω σε αυτόν το χάρτη, να παρουσιάσω καταστήματα που έχουν χωρική πληροφορία και θα εμφανιστούν στον χάρτη μας. Τι χρειάζομαι ως business πληροφορία; Ένα collection από καταστήματα (τύπου ObservableCollection που μπορεί να ενημερώνει όταν επιδέχεται αλλαγές) και ίσως ένα SelectedStore για να γνωρίζω πιο store έχει επιλεγχθεί (για απεικόνιση περισσότερων πληροφοριών).

public class StoreMapViewModel : ViewModel<IStoreMapView>, IStoreMapViewModel    {        private BusinessLogicDomainContext domainContext = new BusinessLogicDomainContext();        public StoreMapViewModel()        {            Load();        }        private ObservableCollection<Store> stores;        public ObservableCollection<Store> Stores        {            get            {                return stores;            }            set            {                stores = value;                PropertyChanged.Raise(() => Stores);            }        }        private Store selectedStore;        public Store SelectedStore        {            set            {                selectedStore = value;                PropertyChanged.Raise(() => SelectedStore);            }            get            {                return selectedStore;            }        }        private void Load()        {           //Εδώ υπάρχει κώδικας για ανάκτηση όλων των stores. Χρησιμοποιήθηκε το WCF Ria Services κομμάτι του WCF           //το οποίο σχεδιάστηκε ειδικά για τις ανάγκες Line of Business silverlight applications            LoadOperation<Store> loadedStores = this.domainContext.Load(this.domainContext.GetStoresQuery());            loadedStores.Completed += (sender, e) => { Stores = new ObservableCollection<Store>(domainContext.Stores); };        }        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;    }

Αναφορικά με το όμορφο API για το raising των property changed events, προκειμένου να γράφουμε κάτι του στυλ Raise(“PropertyName”), θεωρώ πιο όμορφo να χρησιμοποιήσω έναν type safe τρόπο με lambda που na παρέχει το property name, αντί να γράφω κάτι error prone όπως το όνομα του property με το χέρι. (Credits to original author)

public static void Raise(this PropertyChangedEventHandler handler, Expression<Func<object>> propertyExpression) if (handler != null) {     // Retreive lambda body     var body = propertyExpression.Body as MemberExpression;     if (body == null)         throw new ArgumentException("'propertyExpression' should be a member expression");      // Extract the right part (after "=>")     var vmExpression = body.Expression as ConstantExpression;     if (vmExpression == null)         throw new ArgumentException("'propertyExpression' body should be a constant expression");      // Create a reference to the calling object to pass it as the sender     LambdaExpression vmlambda = System.Linq.Expressions.Expression.Lambda(vmExpression);     Delegate vmFunc = vmlambda.Compile();     object vm = vmFunc.DynamicInvoke();      // Extract the name of the property to raise a change on     string propertyName = body.Member.Name;     var e = new PropertyChangedEventArgs(propertyName);     handler(vm, e); }

Bing Maps View

To View μας που περιέχει το bing maps control, είναι ένα μάθημα για databinding με XAML, templating κτλ από μόνο του, αλλά ήθελα να το δείξω εδώ για πληρότητα. Παρατηρήστε πόσο εύκολα και κατανοητά δηλώνω το που αντλώ πληροφορία για τα pushpins (Stores collection) και πιό member διαλέγω για να ανακτήσω την πληροφορία (LocationInformation).

<controls:ViewControl     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"     xmlns:m="clr-namespace:Microsoft.Maps.MapControl;assembly=Microsoft.Maps.MapControl"     mc:Ignorable="d"     xmlns:controls="clr-namespace:Infrastructure.Controls;assembly=Infrastructure"     d:DesignWidth="640" d:DesignHeight="480"     x:Class="Views.StoreMapView">     <UserControl.Resources>         <DataTemplate x:Key="LogoTemplate">             <m:Pushpin m:MapLayer.Position="{Binding LocationInformation}" />         </DataTemplate>     </UserControl.Resources>     <Grid x:Name="LayoutRoot">         <ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}" Margin="0,0,-38,0" >             <StackPanel x:Name="ContentStackPanel" Style="{StaticResource ContentStackPanelStyle}">                 <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"                                    Text="{Binding Path=ApplicationStrings.StoreMapPageTitle, Source={StaticResource ResourceWrapper}}"/>                 <StackPanel x:Name="MapControl" Orientation="Horizontal">                     <m:Map x:Name="MyMap" CredentialsProvider=<!--Εδώ τοποθετούνται τα δικά μας credentials--> Width="519" HorizontalAlignment="Left">                                        <m:MapItemsControl x:Name="ListOfItems"                                     ItemTemplate="{StaticResource LogoTemplate}"                                     ItemsSource="{Binding Stores}">                         </m:MapItemsControl>                     </m:Map>                     <ComboBox Height="23" Name="comboBox1" Width="150" VerticalContentAlignment="Top"                                                           VerticalAlignment="Top" ItemsSource="{Binding Stores}" DisplayMemberPath="storeName"                                                           SelectionChanged="comboBox1_SelectionChanged" />                 </StackPanel>             </StackPanel>         </ScrollViewer>     </Grid> </controls:ViewControl>

 

Ελπίζω να έδωσα αρκετό food for thought. Enjoy Silverlight & MVVM!!!

Posted: Πέμπτη, 11 Μαρτίου 2010 2:30 πμ από το μέλος George J. Capnias
Δημοσίευση στην κατηγορία: , , ,

Σχόλια:

Χωρίς Σχόλια

Ποιά είναι η άποψή σας για την παραπάνω δημοσίευση;

(απαιτούμενο)

(απαιτούμενο)

(προαιρετικό)

(απαιτούμενο)
ÅéóÜãåôå ôïí êùäéêü:
CAPTCHA Image

Ενημέρωση για Σχόλια

Αν θα θέλατε να λαμβάνετε ένα e-mail όταν γίνονται ανανεώσεις στο περιεχόμενο αυτής της δημοσίευσης, παρακαλούμε γίνετε συνδρομητής εδώ

Παραμείνετε ενήμεροι στα τελευταία σχόλια με την χρήση του αγαπημένου σας RSS Aggregator και συνδρομή στη Τροφοδοσία RSS με σχόλια