wtorek, 29 listopada 2011

State-Based Navigation w Prism - interakcja z użytkownikiem

To już ostatni post związany z State-Based Navigation w Prism. W tym poście omówię w jaki sposób prowadzić interakcję z wcześniej stworzoną aplikacją.
Jak już wcześniej wspominałem State-Based Navigation nie nadaje się do skomplikowanych scenariuszy interakcji. Do tego celu służy View Based Navigation, któremu poświęcę kolejne posty.
Naszym zadaniem będzie napisanie prostego okienka umożliwiającego edycję danych zaznaczonej osoby.
Przykład jest bardzo prosty i obrazuje prostą czynność jaką jest edycja w kolekcji, zapis do bazy danych zostawiam dla Was do implementacji :). Aby nie komplikować zbytnio przykładu nie tworzę tutaj żadnego serwisu odpowiedzialnego za okna dialogowe, a użyję toolkitowego ChildWindow.

Najpierw XAML


Code:
<UserControl x:Class="CustomerModule.CustomerListView"
             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:extToolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit/extended"
             mc:Ignorable="d" >
    <UserControl.Resources>
        <DataTemplate x:Key="ItemStandardTemplate">
            <StackPanel>
                <TextBlock>
                    <TextBlock.Text>
                        <MultiBinding StringFormat="{}{0} {1}" >
                            <Binding Path="FirstName" />
                            <Binding Path="LastName" />
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </StackPanel>
        </DataTemplate>
        
        <DataTemplate x:Key="ItemSpecialTemplate">
            <StackPanel Margin="5">
                <Image Source="Images/Person.png" Width="30" Height="30" />
                <TextBlock Text="{Binding EmailAddress}" VerticalAlignment="Center" />
                <TextBlock VerticalAlignment="Center">
                    <TextBlock.Text>
                        <MultiBinding StringFormat="{}{0} {1}" >
                            <Binding Path="FirstName" />
                            <Binding Path="LastName" />
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0">
            <ToggleButton Content="Zmień styl" Name="btnStyleIndicator" />
            <Button Content="Edytuj" Command="{Binding ShowWindow}" /> 
        </StackPanel>
        
        <extToolkit:BusyIndicator IsBusy="{Binding IsBusy}" BusyContent="Loading data..." Grid.Row="1">
            <ListBox ItemsSource="{Binding CustomerList, Mode=TwoWay}" SelectedItem="{Binding SelectedCustomer}">
                <ListBox.Style>
                    <Style TargetType="ListBox">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding ElementName=btnStyleIndicator, Path=IsChecked}" Value="True">
                                <Setter Property="ItemTemplate" Value="{StaticResource ItemSpecialTemplate}" />
                                <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
                                <Setter Property="ItemsPanel">
                                    <Setter.Value>
                                        <ItemsPanelTemplate>
                                            <WrapPanel />
                                        </ItemsPanelTemplate>
                                    </Setter.Value>
                                </Setter>
                            </DataTrigger>

                            <DataTrigger Binding="{Binding ElementName=btnStyleIndicator, Path=IsChecked}" Value="False">
                                <Setter Property="ItemTemplate" Value="{StaticResource ItemStandardTemplate}" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ListBox.Style>
            </ListBox>
        </extToolkit:BusyIndicator>
        
        <extToolkit:ChildWindow IsModal="True" WindowState="{Binding WindowState}" WindowStartupLocation="Center">
            <Grid DataContext="{Binding SelectedCustomer}">
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                
                <TextBlock Text="Imię:" Grid.Row="0" Grid.Column="0"/>
                <TextBox Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="0" Grid.Column="1"/>

                <TextBlock Text="Nazwisko:" Grid.Row="1" Grid.Column="0"/>
                <TextBox Text="{Binding LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1" Grid.Column="1"/>

                <TextBlock Text="Email:" Grid.Row="2" Grid.Column="0"/>
                <TextBox Text="{Binding EmailAddress, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="2" Grid.Column="1"/>
                
            </Grid>
        </extToolkit:ChildWindow> 
    </Grid>
</UserControl>

Nowe rzeczy które się tutaj pojawiły:
  • Dodanie przycisku edycji konkretnego klienta - bindowanie do komendy ShowWindow
  • Dodanie okienka prezentującego dane, oraz możliwość ich zmiany ChildWindow. 
Jak można zauważyć do okienka modyfikacji przekazywany jest aktualnie zaznaczony klient na liście.

ViewModel:


Code:
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BusinessObjects;
using Infrastructure;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Windows.Controls;
using Repositories.Customers;

namespace CustomerModule
{
    public class CustomerListViewModel : BaseViewModel
    {
        private readonly ICustomerRepository _customerRepository;

        private List<Customer> _customerList; 
        public List<Customer> CustomerList
        {
            get
            {
                IsBusy = true;
                if(_customerList != null)
                {
                    IsBusy = false;
                    return _customerList;
                }
                var loadCustomerTask = new Task(() =>
                                                    {
                                                        Thread.Sleep(5000);
                                                        CustomerList = _customerRepository.GetAllCustomers();
                                                        IsBusy = false;
                                                    });
                
                loadCustomerTask.Start();
                return _customerList;
            }
            set
            {
                _customerList = value;
                RaisePropertyChanged("CustomerList");
            }
        }

        private bool _isBusy;
        public bool IsBusy
        {
            get { return _isBusy; }
            set
            {
                _isBusy = value;
                RaisePropertyChanged("IsBusy");
            }
        }

        private Customer _selectedCustomer;
        public Customer SelectedCustomer
        {
            get { return _selectedCustomer; }
            set
            {
                _selectedCustomer = value;
                ShowWindow.RaiseCanExecuteChanged();
                RaisePropertyChanged("SelectedCustomer");
            }
        }

        private WindowState _windowState;
        public WindowState WindowState
        {
            get { return _windowState; }
            set
            {
                _windowState = value;
                RaisePropertyChanged("WindowState");
            }
        }

        public DelegateCommand ShowWindow { get; set; } 

        public CustomerListViewModel(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
            ShowWindow = new DelegateCommand(() => WindowState = WindowState.Open, () => SelectedCustomer != null);
        }
    }
}

W ViewModelu dodane zostało obsłużenie komendy oraz właściwość która odpowiada za chowanie/pokazywanie okienka dialogowego do modyfikacji danych.

To ostatni posty z cyklu State-Based Navigation. Jest to najprostszy sposób nawigacji. W kolejnych postach zostanie omówiony View Based Navigation - znacznie bardziej złożony, ale dający przy tym dużo więcej możliwości.

State-Based Navigation w Prism - wyświetlanie informacji w innej postaci

W poprzednim poście omówiłem w jaki sposób można powiadomić użytkownika o zmianie stanu aplikacji. W tym poście pokaże kolejną z technik związanych z State-Based Navigation, tym razem w kontekście wyświetlania danych w różnych formatach.

Przykład oprę na poprzednim projekcie (link), tak więc należy sobie przygotować projekt tylko z zmianą jednego elementu:
Zamiast DataGrid użyjemy ListBox-a.
Kolejnymi zmianami (ale to już ostatnimi) będzie modyfikacja XAML-a - tutaj określimy dwa style:


Code:
<UserControl x:Class="CustomerModule.CustomerListView"
             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:extToolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit/extended"
             mc:Ignorable="d" >
    <UserControl.Resources>
        <DataTemplate x:Key="ItemStandardTemplate">
            <StackPanel>
                <TextBlock>
                    <TextBlock.Text>
                        <MultiBinding StringFormat="{}{0} {1}" >
                            <Binding Path="FirstName" />
                            <Binding Path="LastName" />
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </StackPanel>
        </DataTemplate>
        
        <DataTemplate x:Key="ItemSpecialTemplate">
            <StackPanel Margin="5">
                <Image Source="Images/Person.png" Width="30" Height="30" />
                <TextBlock Text="{Binding EmailAddress}" VerticalAlignment="Center" />
                <TextBlock VerticalAlignment="Center">
                    <TextBlock.Text>
                        <MultiBinding StringFormat="{}{0} {1}" >
                            <Binding Path="FirstName" />
                            <Binding Path="LastName" />
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0">
            <ToggleButton Content="Zmień styl" Name="btnStyleIndicator" />
        </StackPanel>
        
        <extToolkit:BusyIndicator IsBusy="{Binding IsBusy}" BusyContent="Loading data..." Grid.Row="1">
            <ListBox ItemsSource="{Binding CustomerList, Mode=TwoWay}">
                <ListBox.Style>
                    <Style TargetType="ListBox">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding ElementName=btnStyleIndicator, Path=IsChecked}" Value="True">
                                <Setter Property="ItemTemplate" Value="{StaticResource ItemSpecialTemplate}" />
                                <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
                                <Setter Property="ItemsPanel">
                                    <Setter.Value>
                                        <ItemsPanelTemplate>
                                            <WrapPanel />
                                        </ItemsPanelTemplate>
                                    </Setter.Value>
                                </Setter>
                            </DataTrigger>

                            <DataTrigger Binding="{Binding ElementName=btnStyleIndicator, Path=IsChecked}" Value="False">
                                <Setter Property="ItemTemplate" Value="{StaticResource ItemStandardTemplate}" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ListBox.Style>
            </ListBox>
        </extToolkit:BusyIndicator>
    </Grid>
</UserControl>

Deklarujemy dwa style:
  • ItemStandartTemplate - standardowy styl kiedy przycisk nie jest wciśnięty
  • ItemSpecialTemplate - styl w którym dane są wyświetlane w postaci zdjęcia oraz podpisu pod nim
Poszczególne style prezentują się następująco:
Standardowy:


Specjalny:


Prosta zmiana (nie wymagana była nawet jedna linijka w kodzie C#) a cieszy oko :). Oczywiście można by tutaj podawać ścieżkę do pliku (np. w klasie Customer dodać można pole ImagePath) co sprawiłoby interakcję na najwyższym poziomie.

poniedziałek, 28 listopada 2011

State-Based Navigation w Prism - powiadamianie o stanie aplikacji

W Prism mamy do dyspozycji dwa kluczowe sposoby poruszania się po aplikacji: State-Based Navigation oraz View Based Navigation. W tym artykule omówię pierwszy z tych sposobów.

Base-State Navigation odnosi się do zmiany stanu kontrolek na formatce czyli np. ukrycie kontrolki, pokazanie, dodawanie animacji itp. Nawigacja ta występuje w obrębie pojedynczego widoku.
Obsługa tego typu nawigacji może być spowodowana różnymi rodzaju zachowaniami np.:
  • zmiana wartości właściwości w ViewModelu
  • interakcja użytkownika z aplikacją (kliknięcie na kontrolkę itp.)
  • zmiana sposobu prezentowania danych użytkownika
Kiedy nie używać Base-State Navigation:
  • kiedy chcemy wyświetlać dane z różnych źródeł 
  • użytkownik chce wykonywać zadania nie związane z tym co aktualnie widzi na ekranie
  • skomplikowane zmiany wyglądu
Po tym krótkim wstępie czas na jakieś przykładowe aplikacje, które pokażą w praktyce o co chodzi w opisanej powyżej nawigacji.

1. Powiadamianie o stanie aplikacji.
W tym przykładzie pokażemy użytkownikowi co dzieje się podczas działania naszej aplikacji.

Na początek stwórzmy nasz projekt który będzie mieć następującą postać:



Mając ten szablon przyda się nam kod typów tutaj występujących:

Customer.cs:


Code:
namespace BusinessObjects
{
    public class Customer
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string CompanyName { get; set; }
        public string EmailAddress { get; set; }
        public string Phone { get; set; }
    }
}

IRepository.cs:


Code:
namespace Repositories
{
    public interface IRepository
    {
    }
}

ICustomerRepository.cs:


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

namespace Repositories.Customers
{
    public interface ICustomerRepository : IRepository
    {
        List<Customer> GetAllCustomers();
    }
}


CustomerRepository.cs:


Code:
using System.Collections.Generic;
using System.Data.SqlClient;
using BusinessObjects;
using Repositories.Customers;

namespace MSSQLRepository.Customers
{
    public class CustomerRepository : ICustomerRepository
    {
        public List<Customer> GetAllCustomers()
        {
            var customerList = new List<Customer>();
            using (var conn = new SqlConnection(Configuration.ConnectionString))
            {
                using(var command = conn.CreateCommand())
                {
                    command.CommandText = "SELECT CustomerID, FirstName, LastName, CompanyName, EmailAddress, Phone " +
                                          "FROM SalesLT.Customer";
                    conn.Open();
                    using(var dr = command.ExecuteReader())
                    {
                        while(dr.Read())
                        {
                            customerList.Add(CustomerMapper(dr));
                        }
                        dr.Close();
                    }
                }
            }

            return customerList;
        }

        private static Customer CustomerMapper(SqlDataReader dr)
        {
            var customer = new Customer
            {Id = int.Parse(dr["CustomerID"].ToString()), FirstName = (string) dr["FirstName"]};
            customer.FirstName = (string)dr["LastName"];
            customer.CompanyName = (string)dr["CompanyName"];
            customer.EmailAddress = (string)dr["EmailAddress"];
            customer.Phone = (string)dr["Phone"];

            return customer;
        }
    }
}

Configuration.cs:


Code:
namespace MSSQLRepository
{
    public class Configuration
    {
        public static string ConnectionString =
            "Data Source=localhost;Initial Catalog=AdventureWorksLT2008R2;Integrated Security=SSPI;";
    }
}

BaseViewModel.cs:


Code:
using System.ComponentModel;

namespace Infrastructure
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

RegionNames.cs:


Code:
namespace Infrastructure
{
    public static class RegionNames
    {
        public static string MainContentRegion = "MainContentRegion";
    }
}

Bootstrapper.cs:


Code:
using System.Windows;
using MSSQLRepository.Customers;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.UnityExtensions;
using Microsoft.Practices.Unity;
using Repositories.Customers;

namespace Sample1
{
    public class Bootstrapper : UnityBootstrapper
    {
        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();
            Container.RegisterType(typeof (ICustomerRepository), typeof (CustomerRepository), new ContainerControlledLifetimeManager());
        }

        protected override IModuleCatalog CreateModuleCatalog()
        {
            var moduleCatalog = base.CreateModuleCatalog();
            var customerModule = typeof (CustomerModuleCustomerModule);
            moduleCatalog.AddModule(new ModuleInfo(customerModule.Name, customerModule.AssemblyQualifiedName));

            return moduleCatalog;
        }

        protected override void InitializeShell()
        {
            Application.Current.MainWindow = (Shell)Shell;
            Application.Current.MainWindow.Show();
        }

        protected override DependencyObject CreateShell()
        {
            return Container.Resolve<Shell>();
        }
    }
}

Shell.xaml.cs:


Code:
<Window x:Class="Sample1.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"
        xmlns:Infrastructure="clr-namespace:Infrastructure;assembly=Infrastructure" Title="Shell" Height="300" Width="300">
    <Grid>
        <ContentControl prism:RegionManager.RegionName="{x:Static Infrastructure:RegionNames.MainContentRegion}" />
    </Grid>
</Window>

Czas na nasz moduł - CustomerModule:


Code:
<UserControl x:Class="CustomerModule.CustomerListView"
             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>
        <DataGrid ItemsSource="{Binding CustomerList, Mode=TwoWay}" />
    </Grid>
</UserControl>

CustomerListViewModel.cs:


Code:
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using BusinessObjects;
using Infrastructure;
using Repositories.Customers;

namespace CustomerModule
{
    public class CustomerListViewModel : BaseViewModel
    {
        private readonly ICustomerRepository _customerRepository;

        private List<Customer> _customerList; 
        public List<Customer> CustomerList
        {
            get
            {
                if(_customerList != null)
                {
                    return _customerList;
                }
                var loadCustomerTask = new Task(() =>
                                                    {
                                                        Thread.Sleep(5000);
                                                        CustomerList = _customerRepository.GetAllCustomers();
                                                    });
                
                loadCustomerTask.Start();
                return _customerList;
            }
            set
            {
                _customerList = value;
                RaisePropertyChanged("CustomerList");
            }
        }

        public CustomerListViewModel(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }
    }
}


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

namespace CustomerModule
{
    public class CustomerModule : IModule
    {
        private readonly IRegionManager _regionManager;

        public CustomerModule(IRegionManager regionManager)
        {
            _regionManager = regionManager;
        }

        public void Initialize()
        {
            _regionManager.RegisterViewWithRegion(RegionNames.MainContentRegion, typeof (CustomerListView));
        }
    }
}

Dla przypomnienia sposób powyższy to View Discovery - czyli widok zostanie załadowany zaraz po uruchomieniu naszej aplikacji.
Opiszmy naszą aplikację - mamy do czynienia z bazą (AdventureWorks) z której są pobierane dane i ładowane do kontrolki Grid. Symulujemy tutaj 5 sekundowe opóźnienie podczas ładowania danych. Co w tym momencie widzi użytkownik? Właściwie nic. Będzie musiał poczekać 5 sekund. Nawet jeśli poczeka (nie wyłączy wcześniej aplikacji) to jednak i tak nie wie do końca co dzieje się w tym momencie.
Należy więc poinformować użytkownika co dzieje się w danym momencie z naszą aplikacją.
W tym celu skorzystamy z kontrolki BusyIndicator dostępnej w pakiecie Extended WPF Toolkit.
Do naszego kodu XAML dokładamy następujące linijki:


Code:
<UserControl x:Class="CustomerModule.CustomerListView"
             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:extToolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit/extended"
             mc:Ignorable="d" >
    <Grid>
        <extToolkit:BusyIndicator IsBusy="{Binding IsBusy}" BusyContent="Loading data...">
            <DataGrid ItemsSource="{Binding CustomerList, Mode=TwoWay}" />
        </extToolkit:BusyIndicator>
    </Grid>
</UserControl>

Do ViewModelu:


Code:
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using BusinessObjects;
using Infrastructure;
using Repositories.Customers;

namespace CustomerModule
{
    public class CustomerListViewModel : BaseViewModel
    {
        private readonly ICustomerRepository _customerRepository;

        private List<Customer> _customerList; 
        public List<Customer> CustomerList
        {
            get
            {
                IsBusy = true;
                if(_customerList != null)
                {
                    IsBusy = false;
                    return _customerList;
                }
                var loadCustomerTask = new Task(() =>
                                                    {
                                                        Thread.Sleep(5000);
                                                        CustomerList = _customerRepository.GetAllCustomers();
                                                        IsBusy = false;
                                                    });
                
                loadCustomerTask.Start();
                return _customerList;
            }
            set
            {
                _customerList = value;
                RaisePropertyChanged("CustomerList");
            }
        }

        private bool _isBusy;
        public bool IsBusy
        {
            get { return _isBusy; }
            set { _isBusy = value;
            RaisePropertyChanged("IsBusy");
            }
        }

        public CustomerListViewModel(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }
    }
}

Użytkownik po uruchomieniu programu otrzyma następujący rezultat:


Jest to znacznie przyjemniejszy widok, niż czekanie przed pustym ekranem - bez wiedzy czy aplikacja jeszcze działa i odpowiada na nasze komendy.

W następnym poście pokaże w jaki sposób za pomocą stylów uzyskać różne widoki naszej aplikacji Prism.

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.