poniedziałek, 26 grudnia 2011

Bridge Pattern

Bridge pattern (most) - strukturalny wzorzec projektowy pozwalający oddzielić abstrakcję obiektu od jego implementacji.

Zobaczmy na diagram klas:


Klasy które uczestniczą w tym wzorcu:
Abstraction - definiuje interfejs abstrackji oraz posiada referencję do implementacji
RefinedAbstraction - rozszerza interfejs abstrakcji
Implementator - definiuje interfejs implementatora. Interfejs ten nie musi być zgodny z abstrakcją. Przeważnie Implementator dostarcza niskopoziomowego interfejsu, a abstrakcja operuje na tym interfejsie.
ConcreteImplementator - implementuje interfejs Implementatora.

Dzięki wzorcowi oddzielamy interfejs od implementacji. Głównym celem jest wydzielenie operacji do interfejsu aby klient i serwis mogli działać oddzielnie.
Zysk z zastosowania wzorca:
  • oddzielenie interfejsu od implementacji
  • zwiększona elastyczność - zarówno interfejs jak i konkretną klasę można rozwijać indywidualnie
  • ukrycie implementacji przed klientem 
Przykład realizacji:


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

namespace BridgePatternExample
{
    public class Program
    {
        static void Main()
        {
            var customers = new Customers{DataContext = new CustomerContext()};
            customers.Add(new Customer{BirthYear = 2000, FirstName = "Mark", Id = 1, LastName = "Poldkowski"});
            customers.Add(new Customer { BirthYear = 2000, FirstName = "Sylwester", Id = 2, LastName = "Markowski" });
            customers.Add(new Customer { BirthYear = 2000, FirstName = "Paweł", Id = 3, LastName = "Szymanowski" });

            customers.Print();
        }
    }

    public abstract class CustomersTable
    {
        public DataContext DataContext { get; set; }

        public abstract void Add(EntityBase  customer);
        public abstract void Remove(EntityBase customer);
        public abstract EntityBase Find(int id);
        public abstract void Print();
    }

    public class Customers : CustomersTable
    {
        public override void Add(EntityBase customer)
        {
            DataContext.AddEntity(customer);
        }

        public override void Remove(EntityBase customer)
        {
            DataContext.RemoveEntity(customer);
        }

        public override EntityBase Find(int id)
        {
            return DataContext.FindEntity(id);
        }

        public override void Print()
        {
            DataContext.PrintCustomers();
        }
    }

    public abstract class DataContext
    {
        public abstract void AddEntity(EntityBase entity);
        public abstract void RemoveEntity(EntityBase entity);
        public abstract EntityBase FindEntity(int id);

        public abstract void PrintCustomers();
    }

    public class CustomerContext : DataContext
    {
        private List<EntityBase> _customers = new List<EntityBase>();

        public override void AddEntity(EntityBase entity)
        {
            _customers.Add(entity);
        }

        public override void RemoveEntity(EntityBase entity)
        {
            _customers.Remove(entity);
        }

        public override EntityBase FindEntity(int id)
        {
            return _customers.FirstOrDefault(entity => entity.Id == id);
        }

        public override void PrintCustomers()
        {
            foreach (var customer in _customers)
            {
                Console.WriteLine(customer);
            }
        }
    }

    public abstract class EntityBase
    {
        public int Id { get; set; }
    }

    public class Customer : EntityBase
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int BirthYear { get; set; }

        public override string ToString()
        {
            return string.Format("{0} {1} {2}", FirstName, LastName, BirthYear);
        }
    }
}

niedziela, 25 grudnia 2011

Prototype Design Pattern

Wzorzec Prototype (prototyp) - mówi o tworzeniu obiektów na podstawie prototypu. Jak inne wzorce z grupy wzorców kreacyjnych, wzorzec ten ukrywa przed klientem proces tworzenia obiektu. W przeciwieństwie do innych obiektów, wzorzec ten tworzy kolejne obiekty na podstawie prototypu - klasy która została zainicjowana początkowymi wartościami. Wykorzystanie tego wzorca nie jest duże w przypadku aplikacji biznesowych, przydaje się on natomiast w przypadku aplikacji używanych przez inżynierów.
W .NET istnieje interfejs ICloneable, który dostarcza metody Clone.
Spójrzmy na diagram klas dla tego wzorca:






Klasy, które uczestniczą we wzorcu:
Prototype - udostępnia interfejs klonowania obiektu
ConcretePrototype1 i ConcretePrototype2 - implementują interfejs Prototype dając możliwość klonowania samego siebie
Client - tworzy nowe obiektu poprzez wywołanie metody Clone

Jak wspominałem wcześniej w .NET istnieje już gotowy interfejs ICloneable. Zobaczmy w jaki sposób można go zaimplementować:


Code:
using System;

namespace PrototypeExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person{FirstName = "Jacek", LastName = "Kowalski", BirthYear = 2000};
            Console.WriteLine(person);
            var clone = person.Clone();
            Console.WriteLine(clone);
        }
    }

    class Person : ICloneable
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int BirthYear { get; set; }

        public override string ToString()
        {
            return string.Format("{0} {1} {2}", FirstName, LastName, BirthYear);
        }

        public object Clone()
        {
            return new Person{FirstName = FirstName, LastName = LastName, BirthYear = BirthYear};
            //equals to: return this.MemberwiseClone();
        }
    }
}

W kodzie w metodzie Clone widać zakomentowany kod. Otóż należy być bardzo uważnym podczas implementowania metody Clone. Chodzi o to iż są dwa rodzaje klonowania - deep copy oraz shell copy. Shell copy wykonuje tzw. płytką kopię czyli przepisuje wartości pól. Deep copy przepisuje to co znajduje się pod danym polem. Jeżeli jakieś pole jest referencją, to w przypadku shell copy przepiszemy tylko tę referencję, jeżeli mamy do czynienia z deep copy - obiekt zostanie skopiowany.

Miejmy powyższe zdanie w pamięci i zgodnie z zaleceniami większości deweloperów, podczas implementowania interfejsu ICloneable implementujmy deep copy.

Długi czas odpowiedzi Visual Studio 2010 SP1

Od jakiegoś czasu miałem program z Visual Studio 2010. Po zainstalowaniu dodatku Service Pack 1 ładowanie Designera zajmowało nawet 55 sekund (zgroza...). W takim tempie nie da się pracować.
Pierwszą myślą oczywiście było to, iż mój sprzęt nie spełnia już wymagań. Postanowiłem jednak zbadać w internecie co może powodować tak wolne przełączanie się między kodem a designerem.
Co dodatkowo zaobserwowałem to to, iż operacją która zajmowała tyle czasu było:

"Loading toolbox content from package 'Microsoft.VisualStudio.IDE.ToolboxControlsInstaller,ToolboxInstallerPackage'{2C298B35-07DA-45F1-96A3-BE55D91C8D7A}
Wrzuciłem frazę w wyszukiwarkę i okazało się, iż problem jest już od dłuższego czasu znany. Pod tym linkiem można znaleźć opis problemu i odpowiedzi użytkowników którzy także doświadczyli mniejszej wydajności podczas pracy z VS http://connect.microsoft.com/VisualStudio/feedback/details/551183/loading-toolbox-content-from-package-takes-55-seconds-or-more

Znajduje się tam także obejście jak pokonać problem. Wystarczy za pomocą edytora rejestru (oczywiście wcześniej tworzymy kopię klucza który zamierzamy wyedytować!) usuwamy wpis:

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\10.0\Packages{2C298B35-07DA-45F1-96A3-BE55D91C8D7A}

Po tej operacji i ponownym uruchomieniu systemu VS na powrót odzyskał sprawność.

Design Pattern Builder

Wzorzec budowniczego (Builder) rozdziela proces tworzenia obiektu od jego reprezentacji dzięki czemu ten sam proces tworzenia obiektu może tworzyć różne reprezentacje.

Własnymi słowami: Builder umożliwia w łatwy sposób konstrukcję skomplikowanych obiektów. Builder otrzymuje informację tylko nt. tego jakiego typu obiekt chcemy stworzyć. Cały proces tworzenia obiektu jest ukryty przed użytkownikiem. Przydaje się zwłaszcza tam, gdzie kod tworzący dany komponent jest powtarzalny.
W przeciwieństwie do wzorca Abstract Factory każdy etap tworzenia obiektu przez Buildera jest modyfikowalny. W przypadku Abstract Factory otrzymujemy obiekt w jednym kroku.

Zobaczmy na diagram klas:

Klasy które występują w tym wzorcu:
Director - tworzy obiekt za pomocą interfejsu dostarczonego przez Builder-a
Builder - definiuje interfejs tworzenia składowych Product-u
ConcreteBuilder - spełnia 3 role:
  • Tworzy obiekty na podstawie interfejsu buildera
  • przechowuje instancję obiektu który wyprodukuje
  • udostępnia interfejs umożliwiający pobranie stworzonego obiektu
Product:
  • reprezentuje obiekt który chcemy stworzyć
  • ConcreteBuilder inicjuje wewnętrzną strukturę obiektu 
 Zobaczmy na przykład. Mamy warsztat samochodowy, który zajmuje się składaniem samochodów osobowych i quadów. Pojazdy te składają się z tych samych elementów, tylko w innej konfiguracji i ilości.
Zobaczmy najpierw na schemat klas:


W diagramie tym klasa VehicleBuilder reprezentuje Buildera. Klasy CarBuilder oraz QuadBuilder to konkretni budowniczy obiektów. WorkShop to obiekt utożsamiany z Direcotr. Vehicle to konkretny produkt, a klasy Wheel, Engine, Doors to składowe produktu.

Zobaczmy na sam kod:


Code:
using System;
using System.Text;

namespace BuilderSample
{
    class Program
    {
        static void Main()
        {
            VehicleBuilder builder = new CarBuilder();
            var workShop = new WorkShop();
            workShop.MakeVehicle(builder);
            builder.Vehicle.Specyfication();
            Console.WriteLine("--------------------------------------------");
            builder = new QuadBuilder();
            workShop.MakeVehicle(builder);
            builder.Vehicle.Specyfication();
        }
    }

    class WorkShop
    {
         public void MakeVehicle(VehicleBuilder vehicleBuilder)
         {
             vehicleBuilder.BuildDoor();
             vehicleBuilder.BuildEngine();
             vehicleBuilder.BuildWhell();
         }
    }

    public class Vehicle
    {
        public Engine Engine { get; set; }
        public Wheel Wheel { get; set; }
        public Doors Doors { get; set; }
        public string Type { get; set; }

        public Vehicle(string type)
        {
            Type = type;
        }

        public void Specyfication()
        {
            if (Engine == null)
            {
                throw new NullReferenceException("Engine can't be null");
            }
            if (Wheel == null)
            {
                throw new NullReferenceException("Whell can't be null");
            }
            if (Doors == null)
            {
                throw new NullReferenceException("Doors can't be null");
            }

            var specyficataion = new StringBuilder();
            specyficataion.Append("Type: ");
            specyficataion.Append(Type);
            specyficataion.AppendLine();
            specyficataion.Append("Door count: ");
            specyficataion.Append(Doors.Count);
            specyficataion.AppendLine();
            specyficataion.AppendLine("Whell specyfication: ");
            specyficataion.Append("Whell count: ");
            specyficataion.Append(Wheel.Count);
            specyficataion.AppendLine();
            specyficataion.Append("Whell producer: ");
            specyficataion.Append(Wheel.Producer);
            specyficataion.AppendLine();
            specyficataion.Append("Doors count: ");
            specyficataion.Append(Doors.Count);
            Console.WriteLine(specyficataion);
        }
    }

    public abstract class VehicleBuilder
    {
        protected Vehicle vehicle;

        public Vehicle Vehicle
        {
            get { return vehicle; }
        }

        public abstract void BuildEngine();
        public abstract void BuildWhell();
        public abstract void BuildDoor();
    }

    public class CarBuilder : VehicleBuilder
    {
        public CarBuilder()
        {
            vehicle = new Vehicle("Car");
        }

        public override void BuildEngine()
        {
            Vehicle.Engine = new Engine{CylindersCount = 4, Power = 100, Type = "Car engine"};
        }

        public override void BuildWhell()
        {
            Vehicle.Wheel = new Wheel{Count = 4, Producer = "Debica"};
        }

        public override void BuildDoor()
        {
            Vehicle.Doors = new Doors{Count = 4};
        }
    }

    public class QuadBuilder : VehicleBuilder
    {
        public QuadBuilder()
        {
            vehicle = new Vehicle("Quad");
        }

        public override void BuildEngine()
        {
            Vehicle.Engine = new Engine{CylindersCount = 2, Power = 50, Type = "Quad engine"};
        }

        public override void BuildWhell()
        {
            Vehicle.Wheel = new Wheel{Count = 4, Producer = "Debowe, quad type"};
        }

        public override void BuildDoor()
        {
            Vehicle.Doors = new Doors{Count = 0};
        }
    }

    public class Doors
    {
        public int Count { get; set; }
    }

    public class Wheel
    {
        public int Count { get; set; }
        public string Producer { get; set; }
    }

    public class Engine
    {
        public string Type { get; set; }
        public double Power { get; set; }
        public int CylindersCount { get; set; }
    }
}

Po uruchomieniu programu zobaczymy na ekranie następujący wynik:


poniedziałek, 19 grudnia 2011

Unity wstrzykiwanie w konstruktorze kolekcji obiektów danego typu

Ostatnio napotkałem na problem wstrzykiwania w konstruktorze kolekcji obiektów danego typu. Zobaczmy na prostą hierarchię klas:


Code:
public abstract  class A
    {
        
    }

    public class B : A
    {
        
    }

    public class C : A
    {
        
    }


Chcąc następnie wstrzyknąć taką hierarchię w konstruktorze klasy jako:


Code:
    public class D
    {
        private readonly IEnumerable<A> _resolvedClasses;

        D(IEnumerable<A> resolvedClasses)
        {
            _resolvedClasses = resolvedClasses;
        }
    }

Otrzymamy następujący błąd:


Aby poprawnie wykonać operację wstrzyknięcia wszystkich klas potomnych po A należy zarejestrować każdy z typów jako nazwany:


Code:
unityContainer.RegisterType<A, B>("B");
unityContainer.RegisterType<A, C>("C");

Następnie dopisujemy linijkę rejestrującą IEnumerable od naszego typu:


Code:
unityContainer.RegisterType<IEnumerable<A>>(new InjectionFactory(x => x.ResolveAll<A>()));

Po takim zabiegu możemy już bez przeszkód korzystać z kolekcji obiektów danego typu w konstruktorach.

piątek, 9 grudnia 2011

Nwigacja w Prism - zależności między widokami czyli INavigationAware oraz IRegionMemberLifetime w rolach głównych

Kolejny artykuł z serii View Based Navigation in Prism. Tym razem kilka przydatnych wiadomości nt. dwóch bardzo przydatnych interfejsów z których na pewno będziemy korzystać nie raz, podczas implementacji nawigacji w naszej aplikacji tworzonej w WPF/Silverlight przy użyciu Prism-a.

W ostatnim poście opisałem w jaki sposób w Prism implementujemy nawigację. Z nawigacją związane są bardzo silnie dwa interfejsy:
  • INavigationAware

Code:
  public interface INavigationAware
  {
    void OnNavigatedTo(NavigationContext navigationContext);

    bool IsNavigationTarget(NavigationContext navigationContext);

    void OnNavigatedFrom(NavigationContext navigationContext);
  }

Metody zawarte w tym interfejsie:
  • IsNavigationTarget - decyduje czy dany widok uczestniczy w procesie nawigacji (domyślnie jeżeli widok/VM nie implementuje tego interfejsu jest przyjmowane true)
  • OnNavigateFrom - metoda wywoływana jest przed przystąpieniem do zmiany widoku
  • OnNavigateTo - metoda wywoływana jest po zmianie widoku (załadowaniu nowego widoku)
Cały proces nawigacji można prześledzić na diagramie zawartym w dokumentacji Prisma:



Jak zobaczyć w praktyce działanie powyższych metod? Skorzystajmy z stworzonego przez nas ostatnio przykładu i dołóżmy dodatkowe elementy
W widokach A i B dokładamy właściwość CurrentTime oraz Counter oraz implementujemy interfejs INavigationAware:


Code:
using System;
using Microsoft.Practices.Prism.Regions;
using System.ComponentModel;

namespace ModuleA
{
    public class ModuleAViewModel : INotifyPropertyChanged, INavigationAware
    {
        public DateTime CurrentTime
        {
            get { return DateTime.Now; }
        }

        private int _counter;
        public int Counter
        {
            get { return _counter; }
            set
            {
                _counter = value;
                RaisePropertyChanged("Counter");
            }
        }

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            Counter++;   
        }

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            return true;
        }

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
        }

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

W VM B kod wygląda tak samo więc nie będę go tutaj dwa razy kopiować.

W widokach dokładamy dodatkowe TextBlock-i do wyświetlenia dodatkowych informacji:


Code:
<UserControl x:Class="ModuleB.ModuleBView"
             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>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Text="Moduł B" />
        <TextBlock Text="{Binding CurrentTime, Mode=OneWay}" Grid.Row="1" />
        <TextBox Text="{Binding Counter}" Grid.Row="2" />
    </Grid>
</UserControl>

Odpalmy aplikację i poprzemieszczajmy się pomiędzy widokami:


Efekt jest taki, że data pozostaje cały czas taka sama, a licznik w obu modułach jest inkrementowany.

Jest to dla nas o tyle ważne, że już teraz widzimy iż nasze widoki nie są niszczone. Przechodząc pomiędzy kolejnymi gałęziami naszego meni nawigacyjnego widoki pod spodem pozostają te same (te same instancje klasy).
Korzystanie z tej samem instancji pomiędzy kolejnymi przemieszczeniami się, jest w większości przypadków dla nas korzystną opcją, jednak nie zawsze. Niekiedy wymagamy aby widok był tworzony za każdym razem od nowa. Aby osiągnąć taki rezultat, należy skorzystać z innego interfejsu:
  • IRegionMemberLifetime
Interfejs ten ma następującą definicję:


Code:
  public interface IRegionMemberLifetime
  {
    bool KeepAlive { get; }
  }

Występuje tutaj jedna właściwość KeepAlive, która definiuje czy dany widok w regionie ma być tworzony za każdym razem od nowa, czy też ma zostać użyta już raz stworzona instancja.
Aby zobrazować zasadę działania, zaimplementujemy ten interfejs w module B:


Code:
using System;
using System.ComponentModel;
using Microsoft.Practices.Prism.Regions;

namespace ModuleB
{
    public class ModuleBViewModel : INotifyPropertyChanged, INavigationAware, IRegionMemberLifetime
    {
        public DateTime CurrentTime
        {
            get { return DateTime.Now; }
        }

        private int _counter;
        public int Counter
        {
            get { return _counter; }
            set
            {
                _counter = value;
                RaisePropertyChanged("Counter");
            }
        }

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

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            Counter++;
        }

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            return true;
        }

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
        }

        public bool KeepAlive
        {
            get { return false; }
        }
    }
}

Zwracamy wartość false więc nasz widok nie powinien być przetrzymywany w pamięci. Co to oznacza w przypadku rozważanego przykładu?
Zobaczmy efekt działania po kilkakrotnym przemieszczeniu się pomiędzy modułami A i B



Na pierwszym screenie widać Moduł A, na drugim B. Licznik pierwszego modułu ma wartość 9 a drugiego 1. Także czas na pierwszym screenie nie zmienił się pomimo upływu go podczas przemieszczania się między widokami jak to ma miejsce w module A.
Z tego wyciągamy wnioski, iż moduł A został raz zainicjowany i jego instancja była używana w dalszej nawigacji. Moduł B za każdym razem był inicjowany od nowa.



Omówione tutaj interfejsy z pewnością nie raz przydadzą się nam w naszych aplikacjach w przypadku nawigacji.

środa, 7 grudnia 2011

Prism View Navigation - Region Navigation

Region Navigation to jeden z rodzajów View Navigation. Mówiąc najprościej jest to podmiana widoku aktualnie wyświetlanego w danym regionie na inny widok.

W Prism mamy do dyspozycji bardzo prostą metodę RequestNavigate. Można ją wywołać na dwa sposoby:
  • za pośrednictwem klasy Region:

Code:
            IRegion myRegion = region;
            myRegion.RequestNavigate("Name_of_view");
Metoda w tym przypadku przyjmuje tylko nazwę widoku który ma zostać wyświetlony w regionie.
  •  za pośrednictwem klasy IRegionManager:

Code:
IRegionManager regionManager = rm;
region.Manager.RequestNavigate(RegionNames.Content, new Uri("View", UriKind.Relative));
W tym przypadku podajemy region w którym ma zostać wyświetlony widok jak i jego nazwę.

W tym przykładzie pokażę w jaki sposób skorzystać z drugiej wersji, czyli za pomocą RegionManagera - co wydaje mi się, daje większe możliwości i większą elastyczność.

Na początek struktura projektu który będziemy tworzyć:


Aplikacja będzie prezentowała się w następujący sposób:






Po kliknięciu w przycisk Moduł A zostanie wyświetlona zawartość modułu A, a w Module B Modułu B - prosta sprawa :).

Myślę że nie będę więcej wklejać kodu, który można znaleźć w poprzednich postach z cyklu Prism. Tak więc tylko najważniejsze części kodu oraz ich opis.

Główne okno aplikacji:


Code:
<Window x:Class="Sample_application_1.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:Core="clr-namespace:Core;assembly=Core" Title="Shell" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
        <StackPanel prism:RegionManager.RegionName="{x:Static Core:RegionNames.ToolbarRegion}" 
                    Grid.Row="0" Orientation="Horizontal"/>
        
        <ContentControl prism:RegionManager.RegionName="{x:Static Core:RegionNames.ContentRegion}" 
                        Grid.Row="1"/>
    </Grid>
</Window>

Tak więc dwa regiony: pierwszy będzie zawierać przyciski nawigacji, drugi treść naszych widoków.

Jako że potrzebujemy wspólnego mechanizmu nawigacji stworzymy w bibliotece Core klasę ApplicationCommands:


Code:
using Microsoft.Practices.Prism.Commands;

namespace Core
{
    public class ApplicationCommands
    {
        public static CompositeCommand NavigationCommand = new CompositeCommand();
    }
}

Wejdźmy do modułu A:


Code:
using Core;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Unity;
using ModuleA.Navigation;

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

        public ModuleAModule(IRegionManager regionManager, IUnityContainer container)
        {
            _regionManager = regionManager;
            _container = container;
        }

        public void Initialize()
        {
            _regionManager.RegisterViewWithRegion(RegionNames.ToolbarRegion, typeof (ButtonModuleA));
            _container.RegisterType<object, ModuleA>(typeof(ModuleA).FullName);
        }
    }
}

Ważniejsze aspekty tego co tutaj zostało wykonane:
  • w konstruktorze wstrzykujemy sobie potrzebne serwisy - kontener oraz region manager
  • standardowo za pomocą ViewDiscovery dodajemy nasz przycisk do Toolbara
  • Najważniejsze: rejestrujemy nasz widok A w kontenerze jako reprezentację typu object. Jest to bardzo ważne i jeżeli chcemy skorzystać z dobrodziejstwa nawigacji musimy w ten sposób zarejestrować nasz widok. 
Zobaczmy następnie jaki kod zawiera ViewModel:


Code:
using Core;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Regions;

namespace ModuleA
{
    public class ModuleAViewModel
    {
    }
}

Właściwie to nic nie ma tutaj ciekawego. Tak samo będzie wyglądać VM drugiego modułu.

Chcemy aby nasz model nawigacji nie był związany z konkretnym widokiem. Implementacja poruszania się po naszej aplikacji zostanie stworzona w VM Shell-a. Zobaczmy więc na kod który znajduje się w Shell-u:


Code:
using Core;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Regions;

namespace Sample_application_1
{
    public class ShellViewModel
    {
        private readonly IRegionManager _regionManager;
        public DelegateCommand<object> NavigationCommand { get; private set; } 

        public ShellViewModel(IRegionManager regionManager)
        {
            _regionManager = regionManager;
            NavigationCommand = new DelegateCommand<object>(navigationPath =>
                                                                {
                                                                    if (navigationPath != null)
                                                                    {
                                                                        regionManager.RequestNavigate(RegionNames.ContentRegion, navigationPath.ToString());
                                                                    }
                                                                });
            ApplicationCommands.NavigationCommand.RegisterCommand(NavigationCommand);
        }
    }
}


Kolejno idąc tokiem kodu:
  • tworzymy deklarację komendy NavigateCommand która ma za zadanie przenieść nas do odpowiedniego modułu
  • W konstruktorze tworzymy ją i za pomocą RequestNavigate przenosimy się do odpowiedniego widoku.
  • Jednym z parametrów metody jest navigationPath, która zostanie przysłana jako parametr komendy a decyduje ona o adresie widoku do którego chcemy się przenieść
  • Rejestrujemy naszą komendę w CompositeCommand NavigationCommand

Widok przycisku Modułu A:


Code:
<UserControl x:Class="ModuleA.Navigation.ButtonModuleA"
             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" 
             xmlns:Core="clr-namespace:Core;assembly=Core" 
             xmlns:ModuleA="clr-namespace:ModuleA">
    <Grid>
        <Button Content="Moduł A" Command="{x:Static Core:ApplicationCommands.NavigationCommand}" 
                CommandParameter="{x:Type ModuleA:ModuleA}"/>
    </Grid>
</UserControl>

Bindujemy się do naszej komendy, a następnie jako parametr przekazujemy nazwę widoku, pobraną z enuma:


Code:
namespace ModuleA
{
    public enum ViewType
    {
        ModuleA
    }
}


Teraz możemy włączyć naszą aplikację i przemieszczać się pomiędzy widokami.

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.