Thursday, November 27, 2014

Динамическая загрузка View в Prism 5

Сегодня мы снова коснемся темы использования регионов в Prism 5. В прошлой теме я затронул использование View Discovery и View Injection в Prism 5. Как я и предполагал, это вызвало вопрос: "Что делать, если нужно в конкретный регион подгрузить представление (view) динамически?". Именно это и будет сегодняшней темой данной статьи, а именно: один из способов динамического управления представлениями с помощью внедрения зависимостей (view injection). Мы не будем злоупотреблять теорией и построим самый пример, так как основной целью является показать, что вы можете без особого труда использовать это в своих программах. В следующих статьях рассмотрим более сложный способ навигации по регионам и управление ими с помощью View-Switching Navigation, хотя мне больше по душе старое название данного типа навигации в Prism 5 – View Based Navigation. Существует два способа работы с представлениями в Prism 5:
  • обнаружение представлений (View Discovery), которое позволяет при старте автоматически указать, как связать конкретный регион с нужным представлением;
  • внедрение представлений (View Injection), которое позволяет программно связать представление с регионом как при старте программы та и динамически во время работы программы.
Как говорят в народе, “меньше слов, больше дела”; приступим к реализации. Итак, что же будет собой представлять наша программа. Это будет приложение, в котором мы создадим в главном окне регион “MainRegion”, а также отдельно два UserControl, один из которых будет загружен при старте программы в наш регион, а второй будет загружен динамически по нажатию кнопки в первом контроле. Вроде ничего сложного нет, поэтому приступаем к реализации. Для этого заходим в Visual Studio и создаем новый WPF проект с выбором фреймворка 4.5 для того чтобы использовать призм 5-й версии. Назовем наш проект “DynamicViewInjection”, чтобы он отражал суть нашей работы.
Ниже на рисунке показано стартовое окно со всеми настройками. Затем сразу через Manager NuGet Packages загрузим Prism и Prism.UnityExtensions, для того чтобы в качестве загрузчика приложения использовать UnityBootstrapper, который работает совместно с IoC контейнером Unity.
Ребята с Patterns & Practices старались так сильно все разбить, чтобы библиотеки Prism 5 можно было использовать по отдельности, что это привело к тому, что если мы ставим сразу данную библиотеку целиком и плюс к этому ставим UnityBootstrapper загрузчик, то в итоге получим ни много ни мало 11 библиотек.
Лично мне не очень нравится такое огромное количество библиотек. Но если учесть, что в Prism 5 есть практически все для построения корпоративных приложений, то такое количество может быть оправдано. Я немного отошел от темы, поэтому вернемся к разметке нашего проекта. Поскольку с Prism 5 использование MVVM – это,по сути, просто необходимость, иначе если вы не используете MVVM, тогда не совсем понятно, зачем вам вообще нужен Prism. Это я веду к тому, что структура папок будет разбита согласно концепции Prism + MVVM. У меня это выглядит так:
Чтобы не расписывать отдельно каждую папку, я решил сделать это в виде рисунка. Поскольку мы работаем в призме, нам необходимо и оперировать терминами призма. Главное окно в Prism 5 называется оболочкой Shell. Поэтому мы перенесем наше окно сразу в папку Views и переименуем его с “MainWindow” на “Shell”. После этого нужно перейти в разметку App.xaml и удалить StartupUri, который запускает главное окно. Главное окно у нас будет запускаться с помощью загрузчика (bootstrapper) призма. Для этого мы сделаем небольшую заготовку, в которой дальше вставим код запуска нашего bootstrapper. Переходим в файл App.xam.cs и переопределяем метод OnStartup.
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
    }
}
Если вы все проделали правильно, как минимум ваш проект должен собраться. Если что-то не получилось, то, в принципе, ничего страшного. Просто откройте эту статью: "Введение в Prism 5. Bootstrapper" и шаг за шагом делайте то, что там прописано. Теперь наша задача – добавить загрузчик, в котором мы пропишем инициализацию нашего приложения. Для этого в проект добавим новый класс Bootstrapper.cs. Реализация этого класса приведена ниже.
public class Bootstrapper : UnityBootstrapper
{
    protected override DependencyObject CreateShell()
    {
        return Container.Resolve<Shell>();
    }

    protected override void InitializeShell()
    {
        Application.Current.MainWindow = (Window)Shell;
        Application.Current.MainWindow.Show();
    }
}
Затем переходим в наш класс App.xaml.cs и добавляем в заготовку метода OnStartup следующий код:
protected override void OnStartup(StartupEventArgs e)
{
    var bootstrapper = new Bootstrapper();
    bootstrapper.Run();
}
После этого, если вы запустите проект, то должны увидеть на экране пустое окно.
Теперь самое время приступить к реализации контролов, которые будут заполнять наш регион. Для начала сделаем разметку в нашей оболочке Shell.xaml. В ней нет ничего сложного, просто ContentControl, который будет отображать наше содержимое.
<Window x:Class="DynamicViewInjection.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:regions="http://www.codeplex.com/CompositeWPF"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ContentControl regions:RegionManager.RegionName="MainRegion" />
    </Grid>
</Window>
Затем переходим в папку Regions и добавляем туда новый UserControl с именем FirstView. Реализация этого окна приведена ниже.
<UserControl x:Class="DynamicViewInjection.Views.Regions.FirstView"
             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 Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="50"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Border Background="Gold" Margin="2" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0">
            <StackPanel Orientation="Horizontal">
                <Button Content="Button1" Margin="3,2"/>
                <Button Content="Button2" Margin="3,2"/>
            </StackPanel>
        </Border>

        <Grid Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Background="ForestGreen">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <ComboBox Grid.Column="0" />
        </Grid>

        <StackPanel Grid.Column="0"  Grid.Row="2" Background="DodgerBlue" Margin="5,3"/>
        <StackPanel Grid.Column="1"  Grid.Row="2" Background="DodgerBlue" Margin="5,3"/>

        <StackPanel Grid.Column="0"  Grid.Row="3" Grid.ColumnSpan="2" Background="Firebrick" Margin="5,10"/>
    </Grid>
</UserControl>
Внешний вид контрола приведен ниже.
Я старался эмулировать окно, как в реальном приложении, поэтому цветами выделил те области, вместо которых при нормальной разметке можно использовать регионы для заполнения.
Ну и добавим второй контрол, с названием которого тоже не будем сильно изобретать  и назовем его просто: SecondView.
<UserControl x:Class="DynamicViewInjection.Views.Regions.SecondView"
             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 Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid Grid.Row="0" Background="ForestGreen">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <ComboBox Grid.Column="0" />
        </Grid>

        <StackPanel Grid.Column="0"  Grid.Row="1" Background="DodgerBlue" Margin="5,3"/>

        <Border Background="Gold" Margin="2" Grid.Row="1" Height="25" HorizontalAlignment="Left" VerticalAlignment="Top">
            <StackPanel Orientation="Horizontal">
                <Button Content="Button1" Margin="3,2"/>
                <Button Content="Button2" Margin="3,2"/>
                <Button Content="Button3" Margin="3,2"/>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>
В этом контроле для разнообразия я добавил три кнопки, чтобы показать, что это другое представление.
Теперь сделаем так, чтобы первый контрол по умолчанию загружался в наш главный регион, который мы добавили в Shell.xaml с названием “MainRegion”. Для этого нам необходимо перейти в папку Modules и добавить новый класс MainModule.cs.
public class MainModule : IModule
{
    private readonly IRegionManager _regionManager;
    public readonly IUnityContainer _container;

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

    public void Initialize()
    {
        _regionManager.Regions["MainRegion"].Add(_container.Resolve<FirstView>());
    }
}
Теперь для того чтобы это чудо заработало, нам необходимо добавить инициализацию нашего модуля в созданный ранее бутстраппер. В нем добавится всего лишь один переопределённый метод InitializeModules.
public class Bootstrapper : UnityBootstrapper
{
    protected override DependencyObject CreateShell()
    {
        return Container.Resolve<Shell>();
    }

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

    protected override void InitializeModules()
    {
        IModule mainModule = Container.Resolve<MainModule>();
        mainModule.Initialize();
    }
}
Теперь запустим наш проект, чтобы посмотреть, что наше окно отображает уже заполненный регион. Теперь пришло время для написания логики. Мы сделаем так, чтобы нажатие на первую кнопку контрола FirstView загружало в регион контрол SecondView, а нажатие на вторую кнопку в SecondView, наоборот, загружало FirstView. То есть, контролы загружают друг друга. Для этого нам понадобится реализовать модель представления. Для этого нужно перейти в папку ViewModels и добавить класс FirstViewModel для имплементации логики FirstView, и аналогично − класс SecondViewModel для второго контрола. Ниже приведена реализация FirstViwModel.
public class FirstViewModel : BindableBase
{
    #region Private Variables
    private readonly IRegionManager _regionManager;
    private readonly IUnityContainer _unityContainer;
    #endregion

    #region Constructor
    public FirstViewModel(IRegionManager regionMananger, IUnityContainer container)
    {
        _regionManager = regionMananger;
        _unityContainer = container;
        ButtonCommand = new DelegateCommand(ShowSecondView);
    }
    #endregion

    #region Commands
    public DelegateCommand ButtonCommand { get; private set; }

    #endregion

    #region Private Methods
    private void ShowSecondView()
    {
        var view = _unityContainer.Resolve<SecondView>();
        view.DataContext = _unityContainer.Resolve<SecondViewModel>();
        _regionManager.Regions["MainRegion"].Add(view);
        _regionManager.Regions["MainRegion"].Activate(view);
    }
    #endregion
}
Ничего сложного в данной модели представления для вас не должно быть. Здесь, по сути, только одна команда ButtonCommand, которая реализует метод ShowSecondViewдля того чтобы с помощью view injection заменить в нашем регионе представление на другое. И реализация SecondViewModel выглядит очень похоже.
public class SecondViewModel : BindableBase
{
    #region Private Variables
    private readonly IRegionManager _regionManager;
    private readonly IUnityContainer _unityContainer;
    #endregion

    #region Constructor
    public SecondViewModel(IRegionManager regionMananger, IUnityContainer container)
    {
        _regionManager = regionMananger;
        _unityContainer = container;
        ButtonCommand = new DelegateCommand(ShowFirstView);
    }
    #endregion

    #region Commands
    public DelegateCommand ButtonCommand { get; private set; }

    #endregion

    #region Private Methods
    private void ShowFirstView()
    {
        var view = _unityContainer.Resolve<FirstView>();
        view.DataContext = _unityContainer.Resolve<FirstViewModel>();
        _regionManager.Regions["MainRegion"].Add(view);
        _regionManager.Regions["MainRegion"].Activate(view);
    }
    #endregion
}
У этих классов настолько подобная логика, что для таких целей можно вынести всю логику в базовый класс и просто в производных переопределить метод, который реализует view injection. После того как мы создали наши модели, нам нужно при старте нашего приложения для первого представления FirstView установить DataContext как FirstViewModel. Для этого переходим в наш класс MainModule, в котором осуществляется начальная привязка, и переписываем метод Initialize() вот так:
public void Initialize()
{
    var view = _container.Resolve<FirstView>();
    view.DataContext = _container.Resolve<FirstViewModel>();
    _regionManager.Regions["MainRegion"].Add(view);
}
Теперь осталась самая простая и самая легкая часть, а именно связать кнопки с соответствующими командами с моделей представлений. Первой под нашу правку попадает контрол FirstView. В нем ищем, где находятся кнопки, и для первой кнопки устанавливаем байндинг.
StackPanel Orientation="Horizontal">
    <Button Content="Button1" Margin="3,2" Command="{Binding ButtonCommand}"/>
    <Button Content="Button2" Margin="3,2"/>
</StackPanel>
Теперь переходим на контрол SecondView, ищем там кнопки и теперь уже для второй кнопки устанавливаем байндинг.
<StackPanel Orientation="Horizontal">
    <Button Content="Button1" Margin="3,2"/>
    <Button Content="Button2" Margin="3,2" Command="{Binding ButtonCommand}"/>
    <Button Content="Button3" Margin="3,2"/>
</StackPanel>
После этого вы можете запустить ваше приложение, и у вас должна заработать смена представлений.
Надеюсь, что это небольшое введение поможет вам использовать динамическую подгрузку представлений в ваши регионы, используя для этого view injection. Информацию для понимания, в какую сторону нужно копать глубже, я очень старался донести. 

No comments:

Post a Comment