niedziela, 20 listopada 2011

Zarządzanie interfejsem użytkownika w Prism

Czas na omówienie zagadnienia tworzenia interfejsu użytkownika. Przeważnie rozważa się trzy możliwości tworzenia interfejsu:
  1. Tworzenie aplikacji, która cały widok ma zaprojektowany na "gotowo" w jednym pliku XAML
  2. Poszczególne elementy składowe widoku są umieszczone w osobnych modułach (user controls)
  3. Poszczególne elementy są umieszczone w różnych user controls i dodawane dynamicznie podczas działania programu
Podstawowym elementem każdej aplikacji napisanej w Prism jest Shell. Jak już wiemy z wcześniejszych postów, jest to główne okno naszej aplikacji. Shell podzielony jest na regiony, które korzystając z odpowiednich kontenerów (np. ContentControl, ItemsControl, TabControl) reprezentują dynamicznie tworzone obiekty wizualne w naszej aplikacji.

Jak więc działają regiony? W jaki sposób je tworzymy i inicjujemy?
Regiony
Aby jakiś obszar przeznaczyć na region należy skorzystać z adaptera regionu. Za pomocą adapteru tworzone są obszary w których można tworzyć komponenty wizualne. Prism zawiera 3 gotowe adaptery:
  • ContentControlRegionAdapter - dla kontrolek typu content
  • SelectorRegionAdapter - dla kontrolek typu TabControl
  • SelectorRegionAdapter - dla kontrolek listowych
Regiony mają przyporządkowane tzw. zachowania (behaviors). Zachowania umożliwiają rejestrację komponentów w odpowiednich regionach. Wyróżniamy następujące zachowania:
  • Registration - odpowiada za rejestrację nowego regionu, jak i również usunięcie komponentu po zakończeniu jego używania
  • Auto-population - przetwarza wszystkie zarejestrowane widoki przyporządkowane do danego regionu i tworzy ich instancje. Jeżeli chcemy wpłynąć na sposób działania tego mechanizmu warto zainteresować się interfejsem IRegionViewRegistry oraz AutoPopulateRegionBehavior.
  • Context - odpowiada za synchronizację Context w widoku oraz regionu
  • Activation - powiadamia widok o aktywności lub jej braku. Aby móc odbierać te informacje widok musi implementować interfejs IActiveAware
  • Lifetime - decyduje o usunięciu z danego regionu komponentu. 
Tworzenie widoku w Prism może przebiegać w jednym z dwóch trybów: automatycznego wyszukiwania oraz przez wstrzykiwanie.
Automatyczne wyszukiwanie - (View discovery) ustanawiamy w nim relację pomiędzy nazwą regionu a typem widoku. Po stworzeniu regionu, następuje wyszukanie które widoki są powiązane z danym regionem, a następnie automatycznie załadowane w dany region. Nie ma więc kontroli kiedy widok zostanie stworzony i załadowany.
Wstrzykiwanie regionu  - w kodzie pobieramy referencję do regionu a następnie z poziomu kodu dodajemy do niego region.
Użycie obu metod determinują odpowiednie czynniki:
View discovery:
  • automatyczne ładowanie widoków jest pożądane
  • jeden region odpowiada jednemu widokowi
Wstrzykiwanie regionu:
  • aplikacja korzysta z nawigacji
  • potrzebujemy kontroli nad tworzeniem i usuwaniem widoków
  • potrzebujemy wyświetlić wiele instancji tego samego widoku w regionie ale z różnymi danymi

Deklarowanie regionu
Region deklarujemy w Shell--u (Shell-i może być oczywiście więcej niż jeden jeżeli tylko mamy taką potrzebę). Przykładowa deklaracja regionów:


Code:
<Window x:Class="WpfApplication12.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>
        <ContentControl prism:RegionManager.RegionName="MainRegion" />
    </Grid>
</Window>

RegionName określa nazwę regionu, czyli miejsce w które będą wstrzykiwane stworzone w późniejszym czasie widoki.

Zobaczmy teraz na przykładzie View Discovery oraz View Injection

View Discovery
Jak wcześniej omawialiśmy View Discovery jest praktycznie bezobsługowym trybem ładowania widoków do regionów. Zobaczmy jak się nim posługiwać w aplikacji:
1. Tworzymy nową aplikację w której Bootstrapper wygląda następująco:


Code:
using System.Windows;
using System.Windows.Controls;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Prism.UnityExtensions;
using Microsoft.Practices.Unity;

namespace WpfApplication1
{
    public class Bootstrapper : UnityBootstrapper
    {
        protected override DependencyObject CreateShell()
        {
            return Container.Resolve<Shell>();
        }

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

        protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
        {
            var mappings = base.ConfigureRegionAdapterMappings();
            mappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>());

            return mappings;
        }

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

            return moduleCatalog;
        }
    }
}

Jak widać moduł w katalogu zarejestrowaliśmy z kodu. Sam moduł wygląda następująco:

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

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

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

        public void Initialize()
        {
            _regionManager.RegisterViewWithRegion("ContentRegion", typeof (ModuleAView));
        }
    }
}

Za pomocą metody RegisterViewWithRegion przypisujemy do danego regionu widok. W momencie stworzenia regionu nastąpi przeszukanie widoków które mają się w nim znaleźć. Znalezione widoki zostaną do niego automatycznie dodane.
Zobaczmy resztę kodu:

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}" VerticalAlignment="Center" />
                <TextBlock Text="Nazwisko:" Grid.Row="1" VerticalAlignment="Center" />
                <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding LastName, Mode=TwoWay}" VerticalAlignment="Center" />
            </Grid>
        </StackPanel>
    </Grid>
</UserControl>


Code:
using System.ComponentModel;

namespace ModuleA
{
    public class ModuleAViewModel : INotifyPropertyChanged
    {
        private string _firstName;
        public string FirstName
        {
            get { return _firstName; }
            set
            {
                _firstName = value;
                RaisePropertyChanged("FirstName");
            }
        }

        private string _lastName;
        public string LastName
        {
            get { return _lastName; }
            set
            {
                _lastName = value;
                RaisePropertyChanged("LastName");
            }
        }

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

View Injection
Posłużymy się tym samym przykładem co poprzednio, z tą różnicą iż sami dodamy zawartość do regionu. Zmiana dotyczyć będzie w zasadzie tylko jednej metody - Initialize:

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);
        }
    }
}

W metodzie tej:
  1. Tworzymy nowy obiekt naszego widoku
  2. Zmieniamy zawartość właściwości FirstName
  3. Pobieramy region z RegionManagera
  4. Dodajemy do regionu widok
Zaleta: całkowita kontrola nad procesem tworzenia/usuwania regionów.

Brak komentarzy:

Prześlij komentarz