Separarea UI de logica aplicatiei

In dezvoltarea unei aplicatii WPF, o practica obisnuita consta in separarea interfetei grafice de logica aplicatiei, astfel incat interfata grafica sa poata evolua indiferent de logica aplicatiei si viceversa. Prin realizarea acestei separari, dezvoltatorul interfetei grafice va fi scutit de cunoasterea logicii apicatiei, astfel putandu-se concentra pe deplin doar pe dezvoltarea interfetei grafice. Deasemenea, dezvoltatorul logicii aplicatiei se va putea concentra doar pe parted de business-logic, ignorand pe deplin elementele grafice. Dezvoltatorul logicii aplicatiei nu este nevoit sa cunoasca tehnologia WPF.

Mergand cu rationamentul mai departe, cele doua "componente" ale unei aplicatii - interfata grafica si logica aplicatiei - pot fi implementate in module diferite (assemblies).

Pentru a realiza aceasta separare, va trebui sa eliminam toate referintele din XAML spre rutine definite in code behind (fisierul C# asociat fisierului XAML). In cazul de fata e vorba de elementul Click al elementului XAML Button si orice referire in code behind la elemente din XAML (setari de atribute ale elementelor din XAML din code behind).

Aceasta separare poate fi realizata datorita mecanismului de data binding din WPF, mecanism care permite asociarea de date cu elemente ale interfetei grafice din WPF. Un element WPF se poate lega la o sursa de date, astfel incat starea lui se va actualiza automat odata cu modificarea datelor asociate. Aceasta asociere poate fi realizata in XAML sau in cod C#.

O alta componenta a separarii interfetei grafice de logica aplicatiei o reprezinta conceptul data context. Acest concept permite elementelor WPF sa mosteneasca de la elementele parinte informatii depsre sursa de date folosita la data binding. Acest lucru este realizat prin intermediul proprietatii DataContext continuta de toate elementele WPF. Defapt, DataContext este o proprietate a clasei FrameworkElement, clasa din care mostenesc direct sau indirect toate elementele WPF care suporta data binding. Toate subelementele unui element parinte vor mosteni contextul de date al parintelui. Asta nu inseamna ca un subelement nu poate sa aiba propriul context de date prin suprascrierea proprietatii DataContext.

Sa incepem acum cu separarea. Pornim de la exemplul din articolul "Prima mea aplicatie WPF". In prima faza, vom crea un nou fisier in care vom defini o clasa care va contine intreaga logica a produsului, eliminand astfel logica din fisierul C# asociat fisierului XAML. Vom numi aceasta clasa MainWindowViewModel si o vom declara in fisierul MainWindowViewModel.cs.

In urmatorul pas vom defini proprietati care sa reprezinte fiecare element WPF, astfel: elementelor care afiseaza date simple, gen TextBlock sau Label, li se vor asocia proprietati de tipul System.String de tip read-only (au doar accesorul get public); elementelor care afiseaza colectii de date, gen ItemsControl, ListBox, DataGrid, etc., li se vor asocia proprietati de tipul System.Collections.ObjectModel.ObservableCollection; elementelor de tip Button, CheckBox, RadioButton, etc., care sunt responsabile de executarea anumitor rutine, li se asociaza proprietati de tipul System.Windows.Input.ICommand. Astfel, putem afirma, ca MainWindowViewModel reprezinta un model al vizualizarii.

Mai jos putem vedea definitia si implementarea clasei MainWindowViewModel.

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;

namespace SampleWpf
{
    internal class MainWindowViewModel
    {
        public MainWindowViewModel() { Mesaje = new ObservableCollection<string>(); }

        public string UltimulMesaj
        {
            get { return _ultimulMesaj; }
            private set { _ultimulMesaj = value;
        }
        private string _ultimulMesaj;

        public string Mesaj
        {
            get { return _mesaj; }
            set { _mesaj = value; }
        }
        private string _mesaj;

        public ObservableCollection<string> Mesaje { get; private set; }

        public ICommand AdaugaMesajCommand
        {
            get { return _adaugaMesajCommand ?? (_adaugaMesajCommand = new Command(Execute)); }
        }
        private ICommand _adaugaMesajCommand;

        private void Execute(object commandParam)
        {
            var mesaj = commandParam as string;
            Mesaje.Add(mesaj);
            UltimulMesaj = string.Format("Mesaj adaugat: {0}", mesaj);
            Mesaj = string.Empty;
        }
    }
}

Observam in implementarea proprietatii AdaugaMesajCommand referirea la o clasa Command. Aceasta clasa implementeaza interfata ICommand si arata ca mai jos.

using System;
using System.Windows.Input;

namespace SampleWpf
{
    internal class Command : ICommand
    {
        private readonly Action<object> _execute;

        public Command(Action<object> execute)
        {
            _execute = execute;
        }

        public bool CanExecute(object parameter) { return true; }
        public event EventHandler CanExecuteChanged;
        public void Execute(object parameter) { _execute(parameter); }
    }
}

Interfata ICommand prezinta trei membrii: metoda CanExecute, evenimentul CanExecuteChanged si metoda Execute. Metoda CanExecute intoarce o valoare de tip boolean, care reprezinta starea de executie a obiectului. Vizual, rezultatul acestei metode poate fi urmarit in starea butonul de care este legata comanda, activat/dezactivat. Daca metoda returneaza o valoare false, butonul va fi afisat dezactivat si nu poate fi actionat, altfel el fiind activ si putand fi actionat. Evenimentul CanExecuteChanged notifica cu privire la schimbarile starii de executie. Metoda Execute se va apela ori de cate ori este actionat butonul.

Urmatorul pas consta-n stergerea rutinei asociate evenimentului Click deoarece logica continuta de aceasta rutina se regaseste acum in metoda Execute din clasa MainWindowViewModel. Se elimina referinta la rutina si din codul XAML si se leaga elementele din codul XAML prin binding de proprietatile definite in clasa MainWindowViewModel, nu inainte de a se asocia date context cu instanta clasei MainWindowViewModel. Fisierul XAML va arata astfel:

<Window x:Class="SampleWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewModel="clr-namespace:SampleWpf"
        Title="Prima mea aplicatie" SizeToContent="WidthAndHeight" Background="SkyBlue" >
    <Window.DataContext>
        <viewModel:MainWindowViewModel/>
    </Window.DataContext>

    <StackPanel>
        <TextBlock x:Name="TbMesaj" Margin="4" Text="{Binding UltimulMesaj}" />
        <ListBox x:Name="LstMesaje" Height="200" Width="300" Margin="4" ItemsSource="{Binding Mesaje}" />
        <StackPanel Orientation="Horizontal">
            <TextBox x:Name="TxtMesaj" Width="200" Margin="4" Text="{Binding Mesaj}" />
            <Button Content="Adauga mesaj" Margin="4"
                         Command="{Binding AdaugaMesajCommand}"
                         CommandParameter="{Binding Text, ElementName=TxtMesaj}"
 />
        </StackPanel>
    </StackPanel>
</Window>

Executand codul de mai sus, introducand un mesaj in casuta de editare si actionand butonul, vom observa ca mesajul va fi afisat in controlul lista dar nu va fi sters din casuta de editare iar mesajul informativ de deasupra listei de mesaje nu va fi afisat. Acest lucru se intampla deoarece interfata grafica nu este notificata de schimbarile din instanta clasei MainWindowViewModel. Probabil va veti intreba cum de mesajul adaugat este vizibil in controlul lista. Acest lucru este posibil deoarece controlul lista este legat prin binding (proprietatea ItemsSource) de proprietatea Mesaje, care este de tip ObservableCollection<string>. Clasa ObservableCollection implementeaza interfata INotifyPropertyChanged, interfata care face parte din infrastructura WPF. Aceasta interfata face posibila sincronizarea automata a interfetei grafice cu datele derivate din acesta interfata prin intermediul singurului sau membru de tip event PropertyChangedEventHandler, PropertyChanged.

Avand acum toate aceste date, vom deriva clasa MainWindowViewModel din INotifyPropertyChanged. Codul va arata foarte asemanator cu cel de mai sus, adaugirile efectuate aparand cu un font ingrosat.

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;

namespace SampleWpf
{
    internal class MainWindowViewModel : INotifyPropertyChanged
    {
        public MainWindowViewModel() { Mesaje = new ObservableCollection<string>(); }

        public string UltimulMesaj
        {
            get { return _ultimulMesaj; }
            private set { _ultimulMesaj = value; InvokePropertyChanged("UltimulMesaj");
        }
        private string _ultimulMesaj;

        public string Mesaj
        {
            get { return _mesaj; }
            set { _mesaj = value; InvokePropertyChanged("Mesaj"); }
        }
        private string _mesaj;

        public ObservableCollection<string> Mesaje { get; private set; }

        public ICommand AdaugaMesajCommand
        {
            get { return _adaugaMesajCommand ?? (_adaugaMesajCommand = new Command(Execute)); }
        }
        private ICommand _adaugaMesajCommand;

        private void Execute(object commandParam)
        {
            var mesaj = commandParam as string;
            Mesaje.Add(mesaj);
            UltimulMesaj = string.Format("Mesaj adaugat: {0}", mesaj);
            Mesaj = string.Empty;
        }

        // Implementeaza membrul INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        private void InvokePropertyChanged(string propertyName)
        {
            var h = PropertyChanged;
            if (h != null)
                h(this, new PropertyChangedEventArgs(propertyName));
        }

    }
}

Executand codul, vom avea efectul dorit. Astfel, am realizat separarea interfetei grafice de logica aplicatiei. Observam insa ca aceasta clasa, MainWindowViewModel, contine un model de date al vizualizarii dar si logica produsului situata in implementarea metodei Execute. In practica uzuala se recurge la separarea celor doua prin introducerea unei noi clase, asa numitul model, clasa care va contine doar modelul de date pe care-l va afisa aplicatia. Datele afisate de aplicatie pot veni dintr-o baza de date, fisier, internet, etc. Tot acest model ca constitui legatura cu partea de business-logic. In cazul de fata, modelul si business-logic-ul se confunda din cauza simplicitatii aplicatiei. Prin acesta noua separare se va crea o clasa MainWindowModel, care va contine doar metoda Execute care va contine un argument de tip string reprezentand mesajului de adaugat. Se adauga clasei MainWindowViewModel o variabila membru (field) de tip MainWindowModel, care va fi instantiata in constructorul clasei iar continutul metodei Execute se va inlocui cu un apel al metodei Execute din clasa MainWindowModel.

Pattern-ul descris mai sus este foarte uzual in dezvoltarea aplicatiilor WPF si se regaseste sub denumirea de Model-View-ViewModel (pe scurt MVVM). Implementarea oferita este foarte simpla si acopera doar necesitatile aplicatiei rudimentare descrisa mai sus. Mai multe despre acest pattern intr-un articol viitor in care voi aborda o implementare mai sofisticata a interfetei ICommand precum si notiuni de comunicare intre view-modele.

Cod sursa

Niciun comentariu:

Trimiteți un comentariu