niedziela, 27 listopada 2011

Szerzej o komunikacji w Prism

W Prism mamy dostępnych kilka mechanizmów komunikacji pomiędzy interfejsem użytkownika a modułami składowymi aplikacji.
Wyróżniamy:
  1. DelegateCommand i DelegateCommand<T>
  2. CompositeCommand
  3. EventAggregation
  4. SharedServices
  5. RegionContext

1. DelegateCommand i DelegateCommand<T>
Pozwala na wywoływanie metod przesłanych jako delegaty. Przeważnie dotyczą jednego ViewModelu.
Zobaczmy na przykładowy kod:

Model:


Code:
using System;

namespace Model
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime? LastSaveData { get; set; }
    }
}

ViewModel:


Code:
using System;
using System.ComponentModel;
using Microsoft.Practices.Prism.Commands;
using Model;

namespace ModuleA
{
    public class ModuleAViewModel : INotifyPropertyChanged, IDataErrorInfo
    {
        public string FirstName
        {
            get { return Person.FirstName; }
            set
            {
                Person.FirstName = value;
                RaisePropertyChanged("FirstName");
            }
        }

        public string LastName
        {
            get { return Person.LastName; }
            set
            {
                Person.LastName = value;
                RaisePropertyChanged("LastName");
            }
        }

        public DateTime? SaveTime
        {
            get { return Person.LastSaveData; }
            set
            {
                Person.LastSaveData = value;
                RaisePropertyChanged("SaveTime");
            }
        }

        public DelegateCommand SaveInformationCommand { get; set; }

        public ModuleAViewModel()
        {
            Person = new Person();
            SaveInformationCommand = new DelegateCommand(() =>
                                                             {
                                                                 SaveTime = DateTime.Now;
                                                             });
        }

        protected Person Person { get; set; }

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

        public string this[string columnName]
        {
            get
            {
                var error = string.Empty;
                switch (columnName)
                {
                    case "FirstName":
                        if (string.IsNullOrWhiteSpace(FirstName))
                        {
                            error = "Podaj imię";   
                        }
                        break;
                    case "LastName":
                        if (string.IsNullOrWhiteSpace(LastName))
                        {
                            error = "Podaj imię";
                        }
                        break;
                }

                return error;
            }
        }

        public string Error
        {
            get { throw new NotImplementedException(); }
        }
    }
}

W ViewModelu zaimplementowaliśmy interfejsy: INotifyPropertyChanged oraz IDataErrorInfo. Zaimplementowane zostały celowo w VM a nie w modelu. Model powinien zawierać "czyste" klasy. Powiadamianie o zmianie wartości właściwości oraz błędach powinno być zawsze implementowane w VM.

View:


Code:
<UserControl x:Class="ModuleA.ModuleAView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <StackPanel Orientation="Vertical">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="30" />
                    <RowDefinition Height="30" />
                    <RowDefinition Height="30" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="100" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <TextBlock Text="Imię:" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" VerticalAlignment="Center" />
                <TextBlock Text="Nazwisko:" Grid.Row="1" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" VerticalAlignment="Center" />
            </Grid>
            <Button Content="Zapisz" Command="{Binding SaveInformationCommand}" />
            <TextBlock Text="{Binding SaveTime}" />
        </StackPanel>
    </Grid>
</UserControl>

Po uruchomieniu programu zobaczymy:


Po kliknięciu w przycisk zostanie wyświetlona poniżej niego informacja o aktualnej dacie. Sprawmy jeszcze, aby w przypadku nie możliwości zapisu (błędu na formularzu) nie dało się aktualizować daty - chcemy o tym powiedzieć użytkownik a wiec zablokować możliwość kliknięcia w przycisk.

Aby uzyskać zamierzony efekt modyfikujemy VM:


Code:
using System;
using System.ComponentModel;
using Microsoft.Practices.Prism.Commands;
using Model;

namespace ModuleA
{
    public class ModuleAViewModel : INotifyPropertyChanged, IDataErrorInfo
    {
        public string FirstName
        {
            get { return Person.FirstName; }
            set
            {
                Person.FirstName = value;
                RaisePropertyChanged("FirstName");
                SaveInformationCommand.RaiseCanExecuteChanged();
            }
        }

        public string LastName
        {
            get { return Person.LastName; }
            set
            {
                Person.LastName = value;
                RaisePropertyChanged("LastName");
                SaveInformationCommand.RaiseCanExecuteChanged();
            }
        }

        public DateTime? SaveTime
        {
            get { return Person.LastSaveData; }
            set
            {
                Person.LastSaveData = value;
                RaisePropertyChanged("SaveTime");
            }
        }

        public DelegateCommand SaveInformationCommand { get; set; }

        public ModuleAViewModel()
        {
            Person = new Person();
            SaveInformationCommand = new DelegateCommand(() =>
                                                             {
                                                                 SaveTime = DateTime.Now;
                                                             },
                                                             () => string.IsNullOrWhiteSpace(Error)
                                                             );
        }

        protected Person Person { get; set; }

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

        public string this[string columnName]
        {
            get
            {
                var error = string.Empty;
                switch (columnName)
                {
                    case "FirstName":
                        if (string.IsNullOrWhiteSpace(FirstName))
                        {
                            error = "Podaj imię";   
                        }
                        break;
                    case "LastName":
                        if (string.IsNullOrWhiteSpace(LastName))
                        {
                            error = "Podaj imię";
                        }
                        break;
                }

                return error;
            }
        }

        public string Error
        {
            get
            {
                var error = string.Empty;
                var propeteries = this.GetType().GetProperties();
                foreach (var propery in propeteries)
                {
                    error += this[propery.Name];
                }

                return error;
            }
        }
    }
}

Uzyskany efekt wygląda następująco:


Tak więc kluczowe składowe DelegateCommand wyglądają następująco:
  • ExecuteMethod - metoda która zostanie wykonana
  • CanExecute - decyduje czy można daną metodę wykonać
  • RaiseCanExecuteChanged - weryfikuje czy można wywołać komendę
Jeżeli chcemy przesłać jakiś parametr możemy posłużyć się właściwością CommandParameter a następnie DelegateCommand<T>. Należy pamiętać że T musi być typem referencyjnym. Zobaczmy na przykład:
VM:


Code:
using System;
using System.ComponentModel;
using Microsoft.Practices.Prism.Commands;
using Model;

namespace ModuleA
{
    public class ModuleAViewModel : INotifyPropertyChanged, IDataErrorInfo
    {
        public string FirstName
        {
            get { return Person.FirstName; }
            set
            {
                Person.FirstName = value;
                RaisePropertyChanged("FirstName");
                SaveInformationCommand.RaiseCanExecuteChanged();
            }
        }

        public string LastName
        {
            get { return Person.LastName; }
            set
            {
                Person.LastName = value;
                RaisePropertyChanged("LastName");
                SaveInformationCommand.RaiseCanExecuteChanged();
            }
        }

        public DateTime? SaveTime
        {
            get { return Person.LastSaveData; }
            set
            {
                Person.LastSaveData = value;
                RaisePropertyChanged("SaveTime");
            }
        }

        public DelegateCommand<object> SaveInformationCommand { get; set; }

        public ModuleAViewModel()
        {
            Person = new Person();
            SaveInformationCommand = new DelegateCommand<object>(x =>
                                                                   {
                                                                       if (x != null)
                                                                       {
                                                                           SaveTime = DateTime.Now.AddDays(int.Parse(x.ToString()));
                                                                       }
                                                                   },
                                                             x => string.IsNullOrWhiteSpace(Error)
                                                             );
        }

        protected Person Person { get; set; }

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

        public string this[string columnName]
        {
            get
            {
                var error = string.Empty;
                switch (columnName)
                {
                    case "FirstName":
                        if (string.IsNullOrWhiteSpace(FirstName))
                        {
                            error = "Podaj imię";   
                        }
                        break;
                    case "LastName":
                        if (string.IsNullOrWhiteSpace(LastName))
                        {
                            error = "Podaj imię";
                        }
                        break;
                }

                return error;
            }
        }

        public string Error
        {
            get
            {
                var error = string.Empty;
                var propeteries = this.GetType().GetProperties();
                foreach (var propery in propeteries)
                {
                    error += this[propery.Name];
                }

                return error;
            }
        }
    }
}

V:


Code:
<UserControl x:Class="ModuleA.ModuleAView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <StackPanel Orientation="Vertical">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="30" />
                    <RowDefinition Height="30" />
                    <RowDefinition Height="30" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="100" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <TextBlock Text="Imię:" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" VerticalAlignment="Center" />
                <TextBlock Text="Nazwisko:" Grid.Row="1" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" VerticalAlignment="Center" />
            </Grid>
            <Button Content="Zapisz" Command="{Binding SaveInformationCommand}" CommandParameter="5" />
            <TextBlock Text="{Binding SaveTime}" />
        </StackPanel>
    </Grid>
</UserControl>


2. CompositeCommand 
W odróżnieniu od DelegateCommand jest najczęściej udostępniana globalnie. Komenda ta składa się z innych komend (można sobie ją wyobrazić jako listę komend). Zastosowanie można sobie wyobrazić np. w scenariuszu gdzie mamy kilka widoków które należy zapisać oddzielnie. Jednym z możliwych rozwiązań jest umieszczenie w każdym z widoków przycisku "Zapisz". Dzięki CompositeCommand możemy zapisać za pomocą jednego przycisku dane z wszystkich widoków.
Implementacja: każdy widok zawiera lokalne komendy, które są zarejestrowane w CompositeCommand. Należy pamiętać, iż jeżeli któraś z komend zwraca CanExecute == false, użycie CompositeCommand nie jest możliwe.
Przykładowy kod oprę o poprzednio tworzony przykład. Struktura projektu przedstawia się następująco:


Dodany moduł - Toolbar przedstawia się następująco:
V:


Code:
<UserControl x:Class="Toolbar.ToolbarView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" >
    <Grid>
        <Button Width="100" Height="25" Content="Zapisz wszystko" />
    </Grid>
</UserControl>

Module:


Code:
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Unity;

namespace Toolbar
{
    public class ToolbarModule : IModule
    {
        private readonly IRegionManager _regionManager;
        private readonly IUnityContainer _unityContainer;

        public ToolbarModule(IRegionManager regionManager, IUnityContainer unityContainer)
        {
            _regionManager = regionManager;
            _unityContainer = unityContainer;
        }

        public void Initialize()
        {
            var region = _regionManager.Regions["NavigationRegion"];
            var vm = _unityContainer.Resolve<ToolbarView>();
            region.Add(vm);
        }
    }
}

VM: na razie pozostawimy pusty

W Bootstrapperze dodajemy informacje o nowym module:


Code:
        protected override IModuleCatalog CreateModuleCatalog()
        {
            var moduleAType = typeof (ModuleAModuleAModule);
            var moduleCatalog = new ModuleCatalog();
            moduleCatalog.AddModule(new ModuleInfo(moduleAType.Name, moduleAType.AssemblyQualifiedName));

            var moduleToolbarType = typeof (ToolbarToolbarModule);
            moduleCatalog.AddModule(new ModuleInfo(moduleToolbarType.Name, moduleToolbarType.AssemblyQualifiedName));

            return moduleCatalog;
        }

Po uruchomieniu aplikacja wygląda następująco:


Jak więc widać mamy zakładki - zmieniłem kontrolkę zawartości na TabControl więc jeszcze dla uzupełnienia kod Shell-a:


Code:
<Window x:Class="WpfApplication1.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism" 
        Title="Shell" Height="300" Width="300">
    <Grid>
        <StackPanel Orientation="Vertical">
            <StackPanel Orientation="Horizontal" prism:RegionManager.RegionName="NavigationRegion" />
            <TabControl prism:RegionManager.RegionName="ContentRegion" >
                <TabControl.ItemContainerStyle>
                    <Style TargetType="TabItem">
                        <Setter Property="Header" Value="{Binding Content.DataContext.FirstName}" />
                    </Style>
                </TabControl.ItemContainerStyle>
            </TabControl>
        </StackPanel>
    </Grid>
</Window>

Chcemy uzyskać następujące działanie - możliwość zapisu zarówno na każdej z zakładek oddzielnie jak i na wszystkich za pomocą przycisku umieszczonego na Toolbarze.
Zdefiniujemy więc CompositeCommand i podepniemy pod nią komendy w zakładkach.
Zróbmy jeszcze w międzyczasie przemeblowanie w naszych klasach i wyodrębnijmy bibliotekę np. Infrastructure która będzie zawierać typy dostępne w całej aplikacji - tam umieścimy stworzoną przed chwilą klasę GlobalCommands:


Definicję naszego zadania już znamy czas na kodowanie. Uaktualniamy kod widoku Toolbara:


Code:
<UserControl x:Class="Toolbar.ToolbarView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:Infrastructure="clr-namespace:Infrastructure;assembly=Infrastructure" 
             mc:Ignorable="d" >
    <Grid>
        <Button Width="100" Height="25" Content="Zapisz wszystko" Command="{x:Static Infrastructure:GlobalCommands.SaveAll}" />
    </Grid>
</UserControl>

Po uruchomieniu aplikacji możemy spostrzec, że klawisz Zapisz wszystko jest nieaktywny:


Teraz jedynie co musimy zrobić to zarejestrować komendy:


Code:
        public ModuleAViewModel()
        {
            Person = new Person();
            SaveInformationCommand = new DelegateCommand(() =>
                                                             {
                                                                 SaveTime = DateTime.Now;
                                                             },
                                                         () => string.IsNullOrWhiteSpace(Error)
                );
            GlobalCommands.SaveAll.RegisterCommand(SaveInformationCommand);
        }

Teraz w momencie uzupełnienia brakujących danych na zakładkach, odblokuje się przycisk " Zapisz wszystko". Po jego wciśnięciu zostanie podana na obu zakładkach aktualna data.

3. EventAggregation
Koncepcją EvenAggregation jest luźna komunikacja oparta o zdarzenia. Tak więc mamy do czynienia z obiektami publikującymi (wysyłającymi) zdarzenia i obiektami subskrybującymi je.
W Prism mamy do dyspozycji serwis reprezentowany przez interfejs IEventAggregator. Dostęp do serwisu możemy uzyskać bezpośrednio z kontenera bądź też poprzez wstrzyknięcie w konstruktor poprzez Unity.
Klasą, która jest odpowiedzialna za zarządzanie i powiadamianie subskrybentów jest klasa CompositePresentationEvent<T>. Klasa ta jest generyczna jak widać z jej deklaracji i wymaga parametru tzw. Payload. Payload to dane które mają zostać przesłane do subskrybentów.
EventAggregator udostępnia następujące metody:
  • publish event - możliwość udostępnienia i "odpalenia" zdarzenia
  • Subscribing to Events - "zapisanie" do odbioru zdarzenia przez subskrybentów
  • Unsubscribing - odpinanie zdarzeń
Zapisanie do odbioru wiadomości jest możliwe dzięki jednej z metod, która determinuje sposób użycia odebranego zdarzenia:
  • UIThread - jeżeli chcemy pod odebraniu zdarzenia uaktualnić interfejs graficzny aplikacji (kontrolki)
  • Filtering - umożliwia odfiltrowanie części zdarzeń na które nie chcemy reagować
  • Strong References - jeżeli dane zdarzenie jest odpalane bardzo dużą ilość razy, a widzimy spadek wydajności poprzez ich wywoływanie można skorzystać z tzw. strong reference czyli referencji która nie zostanie usunięta przez garbage collector. Minusem tego rozwiązania jest to, iż należy ręcznie odpiąć zdarzenie po jego użyciu aby została poprawnie zwolniona pamięć.
Czas na konkretny przykład w kodzie.
Skorzystamy znów z poprzedniego przykładu i dodamy do naszego modułu Infrastructure klasę UpdateEvent która będzie informowała o zaistnieniu aktualizacji danych osobowych.
UpdateEvent.cs:


Code:
using Microsoft.Practices.Prism.Events;

namespace Infrastructure
{
    public class UpdateEvent : CompositePresentationEvent<string>
    {
         
    }
}

Rejestrujemy nasze zdarzenie w module, gdzie wprowadzaliśmy dane osobowe:
ModuleAViewModel.cs:


Code:
using System;
using System.ComponentModel;
using Infrastructure;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Events;
using Model;

namespace ModuleA
{
    public class ModuleAViewModel : INotifyPropertyChanged, IDataErrorInfo
    {
        private readonly IEventAggregator _eventAggregator;

        public string FirstName
        {
            get { return Person.FirstName; }
            set
            {
                Person.FirstName = value;
                RaisePropertyChanged("FirstName");
                SaveInformationCommand.RaiseCanExecuteChanged();
            }
        }

        public string LastName
        {
            get { return Person.LastName; }
            set
            {
                Person.LastName = value;
                RaisePropertyChanged("LastName");
                SaveInformationCommand.RaiseCanExecuteChanged();
            }
        }

        public DateTime? SaveTime
        {
            get { return Person.LastSaveData; }
            set
            {
                Person.LastSaveData = value;
                RaisePropertyChanged("SaveTime");
            }
        }

        public DelegateCommand SaveInformationCommand { get; set; }

        public ModuleAViewModel(IEventAggregator eventAggregator)
        {
            _eventAggregator = eventAggregator;
            Person = new Person();
            SaveInformationCommand = new DelegateCommand(() =>
                                                             {
                                                                 SaveTime = DateTime.Now;
                                                                 _eventAggregator.GetEvent<UpdateEvent>().Publish(LastName);
                                                             },
                                                         () => string.IsNullOrWhiteSpace(Error)
                );
            GlobalCommands.SaveAll.RegisterCommand(SaveInformationCommand);
        }

        protected Person Person { get; set; }

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

        public string this[string columnName]
        {
            get
            {
                var error = string.Empty;
                switch (columnName)
                {
                    case "FirstName":
                        if (string.IsNullOrWhiteSpace(FirstName))
                        {
                            error = "Podaj imię";   
                        }
                        break;
                    case "LastName":
                        if (string.IsNullOrWhiteSpace(LastName))
                        {
                            error = "Podaj imię";
                        }
                        break;
                }

                return error;
            }
        }

        public string Error
        {
            get
            {
                var error = string.Empty;
                var propeteries = this.GetType().GetProperties();
                foreach (var propery in propeteries)
                {
                    error += this[propery.Name];
                }

                return error;
            }
        }
    }
}

Subskrypcję zrobimy dla uproszczenia sprawy w TextBoxie na Toolbarze:
V:


Code:
<UserControl x:Class="Toolbar.ToolbarView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:Infrastructure="clr-namespace:Infrastructure;assembly=Infrastructure" 
             mc:Ignorable="d" >
    <Grid>
        <StackPanel Orientation="Horizontal">
            <Button Width="100" Height="25" Content="Zapisz wszystko" Command="{x:Static Infrastructure:GlobalCommands.SaveAll}" />
            <TextBox Text="{Binding EventMessage}" />
        </StackPanel>
    </Grid>
</UserControl>

VM:


Code:
using System.ComponentModel;
using Infrastructure;
using Microsoft.Practices.Prism.Events;

namespace Toolbar
{
    public class ToolbarViewModel : INotifyPropertyChanged
    {
        private readonly IEventAggregator _eventAggregator;
        private string _eventMessage;
        public string EventMessage
        {
            get { return _eventMessage; }
            set
            {
                _eventMessage = value;
                RaisePropertyChanged("EventMessage");
            }
        }

        public ToolbarViewModel(IEventAggregator eventAggregator)
        {
            _eventAggregator = eventAggregator;
            _eventAggregator.GetEvent<UpdateEvent>().Subscribe(x => EventMessage = x);
        }

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

Tak więc po kliknięciu w przycisk zapisz zobaczmy na naszym Toolbarze następujący wynik:



4. Shared Service
Serwisy współdzielone to najczęściej klasy - Singletony. W aplikacji tworzona jest jedna instancja klasy, która następnie jest dostępna poprzez odpowiedni interfejs w całym programie.
Ponieważ nie ma tutaj zbytnio o czym pisać od razu pokaże przykład:
Stworzymy bardzo prostą klasę która będzie mieć za zadanie przechowywanie kolejnych instancji osób (aby nie utrudniać zrobię to w bibliotece Infrastructure):


Code:
namespace Infrastructure
{
    public interface IRepository<T>
    {
        void AddEntity(T entity);
        int Count();
    }
}



Code:
using Model;

namespace Infrastructure
{
    public interface IPersonRepository : IRepository<Person>
    {
    }
}



Code:
using System.Collections.Generic;
using Model;

namespace Infrastructure
{
    public class PersonRepository : IPersonRepository
    {
        private List<Person> personList;

        public PersonRepository()
        {
            personList = new List<Person>();
        }

        public void AddEntity(Person entity)
        {
            personList.Add(entity);
        }

        public int Count()
        {
            return personList.Count;
        }
    }
}

Kolejno tworzymy interfejs IRepository, następnie IRepositoryPerson a następnie implementację PersonRepository.
Rejestrujemy nasz serwis w kontenerze:


Code:
        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();
            Container.RegisterInstance<IPersonRepository>(new PersonRepository());
        }

Użycie nastąpi w momencie kliknięcia na przycisk zapisz:


Code:
        public ModuleAViewModel(IEventAggregator eventAggregator, IPersonRepository personRepository)
        {
            _eventAggregator = eventAggregator;
            _personRepository = personRepository;
            Person = new Person();
            SaveInformationCommand = new DelegateCommand(() =>
                                                             {
                                                                 SaveTime = DateTime.Now;
                                                                 _eventAggregator.GetEvent<UpdateEvent>().Publish(LastName);
                                                                 _personRepository.AddEntity(Person);
                                                                 MessageBox.Show(_personRepository.Count().ToString());
                                                             },
                                                         () => string.IsNullOrWhiteSpace(Error)
                );
            GlobalCommands.SaveAll.RegisterCommand(SaveInformationCommand);
        }

Działanie:
Klikamy pierwszy raz na zapis na pierwszej zakładce:


Otrzymujemy komunikat że w repozytorium po zapisie znajduje się jeden obiekt.
Następnie klikamy na zapisz na drugiej zakładce:


Dostajemy informację o dwóch instancjach w repozytorium.

Region Context
Pozwala współdzielić dane pomiędzy widokiem nadrzędnym i przechowywanymi w nim widokami podrzędnymi.


Aby przedstawić zasadę korzystania z Region Context skorzystajmy z prostego przykładu. Umożliwimy naszej aplikacji wyświetlenie informacji odnośnie zarejestrowanych użytkowników.
Najpierw pokażę strukturę projektu która zostanie stworzona:


Zaczynamy od dodania dwóch widoków i viewModeli do nich:
  • PersonListView
  • PersonDetailsView
Dodajemy kolejną zakładkę do TabControla w naszym module:


Code:
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Unity;

namespace ModuleA
{
    public class ModuleAModule : IModule
    {
        private readonly IRegionManager _regionManager;
        private readonly IUnityContainer _unityContainer;

        public ModuleAModule(IRegionManager regionManager, IUnityContainer unityContainer)
        {
            _regionManager = regionManager;
            _unityContainer = unityContainer;
        }

        public void Initialize()
        {
            var va = _unityContainer.Resolve<ModuleAView>();
            ((ModuleAViewModel) va.DataContext).FirstName = "Jacek";
            var contentRegion = _regionManager.Regions["ContentRegion"];
            contentRegion.Add(va);

            var vb = _unityContainer.Resolve<ModuleAView>();
            ((ModuleAViewModel)vb.DataContext).FirstName = "Marek";
            contentRegion.Add(vb);

            var userList = _unityContainer.Resolve<PersonListView>();
            contentRegion.Add(userList);

            _regionManager.RegisterViewWithRegion("PersonDetails", typeof (PersonDetailsView));
        }
    }
}

Następnie tworzymy kod dla widoku PersonDetailsView:


Code:
<UserControl x:Class="ModuleA.PersonDetailsView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <StackPanel Orientation="Vertical">
            <WrapPanel Orientation="Horizontal">
                <TextBlock Text="Imię: "/>
                <TextBox Text="{Binding SelectedPerson.FirstName}" Width="150" />
            </WrapPanel>

            <WrapPanel Orientation="Horizontal">
                <TextBlock Text="Nazwisko: "/>
                <TextBox Text="{Binding SelectedPerson.LastName}" Width="150" />
            </WrapPanel>

            <WrapPanel Orientation="Horizontal">
                <TextBlock Text="Data modyfikacji: "/>
                <TextBox Text="{Binding SelectedPerson.LastSaveData}" Width="150" />
            </WrapPanel>
        </StackPanel>
    </Grid>
</UserControl>

PersonDetailsViewModel:


Code:
using System.ComponentModel;
using Model;

namespace ModuleA
{
    public class PersonDetailsViewModel : INotifyPropertyChanged
    {
        private Person _selectedPerson;
        public Person SelectedPerson
        {
            get { return _selectedPerson; }
            set
            {
                _selectedPerson = value;
                RaisePropertyChanged("SelectedPerson");
            }
        }

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

Następnie czas na PersonListView:


Code:
<UserControl x:Class="ModuleA.PersonListView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism"
             mc:Ignorable="d">
    <UserControl.Resources>
        <DataTemplate x:Key="PersonDataTemplate">
            <TextBlock>
                <TextBlock.Text>
                    <MultiBinding StringFormat="{}{0} {1}">
                        <Binding Path="FirstName" />
                        <Binding Path="LastName" />
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </DataTemplate>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
        <ListBox Name="lbPersonList" ItemsSource="{Binding PersonList}" ItemTemplate="{StaticResource PersonDataTemplate}" />
        <ContentControl Grid.Row="1" 
                        prism:RegionManager.RegionName="PersonDetails" 
                        prism:RegionManager.RegionContext="{Binding SelectedItem, ElementName=lbPersonList}" />
    </Grid>
</UserControl>

To tutaj w kontrolce ContentControl rejestrujemy chęć użycia RegionContext. W naszym przypadku bindujemy go do wybranego elementu w ListBoxie, który reprezentuje wszystkie zarejestrowane osoby w naszym repozytorium.

PersonListViewModel:


Code:
using System.Collections.Generic;
using System.ComponentModel;
using Infrastructure;
using Model;

namespace ModuleA
{
    public class PersonListViewModel : INotifyPropertyChanged
    {
        private readonly IPersonRepository _personRepository;
        private List<Person> _personList;
        public List<Person> PersonList
        {
            get { return _personList; }
            set
            {
                _personList = value;
                RaisePropertyChanged("PersonList");
            }
        }

        public PersonListViewModel(IPersonRepository personRepository)
        {
            _personRepository = personRepository;
            _personList = _personRepository.GetAllPerson();
        }

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

Ostatnią rzeczą którą musimy wykonać to dodanie zdarzenia które powiadomi nas o zmianie RegionContext-u oraz przypisze właściwości SelectedPerson z PersonDetailsViewModel wartość. Zdarzenie podpinamy w CodeBehind PersonDetailsView:


Code:
using Microsoft.Practices.Prism;
using Microsoft.Practices.Prism.Regions;
using Model;

namespace ModuleA
{
    /// <summary>
    /// Interaction logic for PersonDetailsView.xaml
    /// </summary>
    public partial class PersonDetailsView
    {
        public PersonDetailsView(PersonDetailsViewModel personDetailsViewModel)
        {
            InitializeComponent();
            DataContext = personDetailsViewModel;
            RegionContext.GetObservableContext(this).PropertyChanged += PersonDetailsView_PropertyChanged;
        }

        void PersonDetailsView_PropertyChanged(object sender, System.ComponentModelPropertyChangedEventArgs e)
        {
            var context = (ObservableObject<object>) sender;
            if (context != null)
            {
                var selectedPerson = context.Value as Person;
                if (selectedPerson != null)
                {
                    ((PersonDetailsViewModel) DataContext).SelectedPerson = selectedPerson;
                }
            }
        }
    }
}

Teraz możemy zobaczyć działanie naszej aplikacji:


po wybraniu osoby z listy kontrolka zostaje wypełniona.
Warto tu wspomnieć, że przesyłamy cały obiekt Person. W realnej aplikacji należałoby przesłać samo Id elementu który zaznaczyliśmy na liście. Pobranie wartości nastąpiło by poprzez odpowiednią metodę w repozytorium.

Tyle na temat różnych sposobów przesyłania danych w Prism.

Brak komentarzy:

Prześlij komentarz