Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Паттерн MVVM и страничная навигация

Anna | 17.06.2014 | нет комментариев
Применение страничной навигации довольно актуальная задача для настольных WPF-MVVM приложений.
Разномастных начальств по организации такой навигации в сети довольно.
И, безусловно, Програпрогр не исключение (имеются статьи раз и два).
Взглянув на первую статью Вы узнаете про NavigationService и вероятность пользоваться Hyperlink.
Если перейдете по 2-й ссылке, то узнаете как пользоваться NavigationService в так называемом «Code Behind».
Таким образом, решения полного в этих статьях не представлено (на мой взор).
Хочется заполнить пробел и представить Вашему вниманию, как мне кажется, абсолютно рабочее решение.
Абсолютно не претендую на завершенный компонент для организации страничной навигации.
Буду признателен за пригодные комментарии, поправки и дополнения.
Рчистилище буду, если кому-то моя реализация навигатора окажется пригодной.

Вступление

Все, кто теснее работал с WPF, наверно знакомы с паттерном MVVM (дал ссылки в конце статьи). чай доктрина MVVM несложна и, как минимум, подсознательно должна быть внятной выгода от его применения. Если хотите, Дабы он проявил себя во каждой красе, то как дозволено поменьше логики помещайте в «Code Behind» пользовательских элементов управления (UserControl) и ни в коем случае не используйте прямые ссылки на UI внутри ViewModel’ей. Данный подход даст Вам огромный интерес в виде вероятности тестирования ViewModel’ей отдельно от контролов. Еще одной отличной практикой будет сведение к минимуму создания экземпляров ViewModel’ей напрямую в контролах. Нестрашно, если элемент управления сам для себя создает ViewModel определенного типа – в этом случае легко сложнее будет подложить контролу какую-нибудь тестовую куклу. Напротив будут обстоять дела, когда некоторый родительский контрол будет занят созданием ViewModel’ей для остальных экранов, чай тогда код может превратиться в нетестируемую кучу спагетти. Если за создание ViewModel’ей будут отвечать другие ViewModel’и, то тестировать станет гораздо легче.

Давайте предположим себе приложение с панелью навигации, несколькими экранами и диалоговыми окнами. что-то сходственное представлено ниже.

Мы можем рассмотреть несколько сущностей: основное окно, панель навигации с кнопками, нынешняя страница и диалог над этой страницей. В нашем приложении для страничной навигации дозволено было бы применять HyperLink, подложив взамен кнопок TextBlock с HyperLink в качестве контента. У HyperLink есть качество, указывающее имя Frame, в котором исполнять переход на новую страницу. И как бы все типично, но с применением HyperLink представляется сложным передача странице требуемой ViewModel’и.
Я видел в сети пару решений этой задачи:

  • В событии Frame.Navigated в основном окне приложения через Code Behind дозволено получить доступ к загруженному во фрейм содержимому и подложить туда сделанную там же в Code Behind ViewModel. Таким образом, создание ViewModel’ей для всех страниц будет сосредоточено в одном обработчике с применением длинной портянки if…else if… либо switch. Про то, что тестирование такого «Hard Coded» процесса навигации весьма сложно автоматизировать, я молчу.
  • Иным решением является создание экземпляра Page и ViewModel’и под нее, подкладывание ViewModel’и в DataContext экземпляра Page и вызов Navigate у фрейма с передачей сделанного экземпляра Page. Это решение немножко отменнее предыдущего, но по-бывшему вовсе не «MVVM-way».
  • Третьим решением дозволено назвать применение библиотек PRISM. Она применяется в секторе больших корпоративных приложений для реализации композитного UI. Если знакомы с AngularJS, то осознаете что это. Реализуется некоторый RegionManager, в котором регистрируются части UI. Потом через сделанный администратор вызывается инстанциирование контрола по некоемому псевдониму, также присвоение надобного контекста данных. Данный функционал схож на то, что теснее реализовано в NavigationService WPF.

Первые два решения — очевидный костыль. PRISM же — это целый фреймворк композиции UI. Инвестировать в его постижение, безусловно, стоит, но для маленькихприложений (proof of concept, напр.) применение таких пророческой, как IoC и PRISM, может оказаться нецелесообразным.

Какое простейшее решение могло бы больше-менее гладко вписаться в контекст MVVM? У класса Page в Silverlight есть перегружаемый способ OnNavigatedTo. В этом способе было бы комфортно принимать ViewModel, переданную в NavigationService.Navigate(Uri uri, object navigationContext) вторым параметром. Впрочем в WPF у Page такого способа нет. По крайней мере я не обнаружил его либо чего-то равнозначного. Нам необходим некоторый посредник либо, если хотите, администратор, тот, что будет контролировать переходы по страницам и перекладывать из параметра способа в DataContext необходимую ViewModel. О реализации такого администратора навигации и пойдет речь в данной статье.
В дальнейшем разделе я расскажу о реализации ядра решения, о администраторе навигации. После этого, будет рассказано о том, что необходимо реализовать на UI и ViewModel слоях. Для экономии времени дозволено прочитать раздел «Администратор навигации», а остальное додумать по ходу решения своих задач.
Кому увлекательно сразу взглянуть на код, может переходить в репозиторий на GitHub.

Администратор навигации

Данный администратор реализован в виде синглтона с двойственный проверкой экземляра на null (так называемый Double-Check Locking Singleton, многопоточная версия синглтона). Применение синглтона — это мое предпочтение. Так мне проще контролировать жизненный цикл. Вам допустимо хватило бы и простого статического класса.
Код реализации синглтона глядите ниже.

Singleton

#region Singleton

        private static volatile Navigation instance;
        private static object syncRoot = new Object();

        private Navigation() { }

        private static Navigation Instance
        {
            get 
            {
                if (instance == null) 
                {
                    lock (syncRoot) 
                    {
                        if (instance == null)
                            instance = new Navigation();
                    }
                }

                return instance;
            }
        }
#endregion

В представленном выше коде Вы можете увидеть, что качество Instance я сделал приватным. Так сделано для простоты, Дабы наружу не выглядывало ничего лишнего. Вам же на практике может понадобиться сделать его доступным публично. Взамен приватного свойства экземпляра синглтона я сотворил публичное качество обслуживания навигации Service (типа NavigationService), которое транслирует вызовы через приватный экземпляр синглтона. Дозволено было сделать напротив, но тогда бы все вызовы снаружи доводилось делать через экземпляр, т.е.

Navigation.Instance.Service

взамен

Navigation.Service

Выбирайте вариант, тот, что Вам огромнее нравится. Мне кажется конечный вариант проще, но он требует дополнительной реализации статических свойств и способов. Следственно с реализацией нового функционала может стать выигрышнее открыть качество экземпляра (Navigation.Instance).

Качество Service в этом синглтоне будет беречь ссылку на NavigationService экземпляра Frame, в котором требуется исполнять страничные переходы. Присваивать актуальное значение этой ссылке дозволено как при старте приложения (в обработчике события Loaded основного окна), так и в всякий иной больше поздний момент до вызова одного из способов навигации.

Пример

        public MainWindow()
        {
            InitializeComponent();

            Loaded  = MainWindow_Loaded;
        }

        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            Navigation.Navigation.Service = MainFrame.NavigationService;

            DataContext = new MainViewModel(new ViewModelsResolver());
        }

В примере выше мы назначаем нашему навигатору NavigationService Frame основного окна. Взамен основного окна мог быть всякий контрол, но забирать NavigationService необходимо в событии Loaded данного контрола. До этого события дозволено получить null. Больше подробно жизненный цикл контролов и NavigationService я не постигал.
В качестве альтернативного сценария я мог бы предложить применение ChildWindow из WPF Toolkit Extended, в тот, что встроен еще один Frame. Дозволено в таком случае временно подменить NavigationService в нашем навигаторе, Дабы совершить переход внутри такого диалога. Это позво_permark! } public ICommand GoToPage1Command { get { return _goToPage1Command; } set { _goToPage1Command = value; RaisePropertyChanged(“GoToPage1Command”); } } private void InitializeCommands() { GoToPathCommand = new RelayCommand<string>(GoToPathCommandExecute); GoToPage1Command = new RelayCommand<Page1ViewModel>(GoToPage1CommandExecute); GoToPage2Command = new RelayCommand<Page2ViewModel>(GoToPage2CommandExecute); GoToPage3Command = new RelayCommand<Page3ViewModel>(GoToPage3CommandExecute); } private void GoToPathCommandExecute(string path) { if (string.IsNullOrWhiteSpace(path)) { return; } var uri = new Uri(path); Navigation.Navigate(uri); } private void GoToPage1CommandExecute(Page1ViewModel viewModel) { Navigation.Navigate(Navigation.Page1Alias, Page1ViewModel); }
Обратите внимание, в качестве пути передается псевдоним целевой страницы. Эти псевдонимы я разместил в виде констант в администратор навигации, но вообще лучшее место для них в XML-файле настроек либо легко в каком-то текстовом словаре.

Позже выполнения команды GoToPage1Command будет осуществлен переход на страницу по указанному псевдониму, а в DataContext страницы будет положена ссылка на Page1ViewModel. Таким образом, нам не нужно реализовывать дополнительную логику по приобретению данных обратно из целевой страницы. Она будет трудиться с хранилищем внутри нашей MainViewModel, следственно все метаморфозы мы будем получать механически еще до перехода обратно.
Как бы бы все с ViewModel’ями. Переходим к UI.

Основное окно и сборка Pages

Приведу вновь для комфорта вид тестового приложения.

Слева представлены четыре кнопки. Первая кнопка привязана к команде GoToPathCommand и исполняет переход на Page1 без контекста данных. Позже перехода на страницу без контекста данных взамен актуального значения из ViewModel’и будет подставлено значение из параметра FallbackValue объекта Binding. Остальные кнопки привязаны к «частным» командам с указанным в делегате команды псевдонимом нужной страницы страницы.

Разметка и код основного окна

<Window x:Class="Navigator.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="480" Width="640">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Hidden">
            <StackPanel>
                <Button Content="P 1 w/o data" Command="{Binding GoToPathCommand}" CommandParameter="pack://application:,,,/Pages;component/Page1.xaml"/>
                <Button Content="Page 1" Command="{Binding GoToPage1Command}" CommandParameter="{Binding Page1ViewModel}"/>
                <Button Content="Page 2" Command="{Binding GoToPage2Command}" CommandParameter="{Binding Page2ViewModel}"/>
                <Button Content="Page 3" Command="{Binding GoToPage3Command}" CommandParameter="{Binding Page3ViewModel}"/>
            </StackPanel>
        </ScrollViewer>

        <Frame x:Name="MainFrame" Grid.Column="1" Background="#CCCCCC"/>

    </Grid>
</Window>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Loaded  = MainWindow_Loaded;            
        }

        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            Navigation.Service = MainFrame.NavigationService;

            DataContext = new MainViewModel();
        }
    }

Сборка Pages содержит четыре страницы: Page1, Page2, Page3, Page404. Первые две легко содержат текстовые блоки, привязанные к свойству соответствующей частной ViewModel. Третью я немножко усложнил, Дабы реализовать еще одну задачу MVVM, а именно задачу привязки ListBox.SelectedItems к ViewModel. Это отдельная тема, которая на мой взор заслуживает отдельной статьи. Для интереса можете заглянуть под спойлер разметки ниже.

Разметка Page3


<Page x:Class="Pages.Page3"
             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:tkx="http://schemas.xceed.com/wpf/xaml/toolkit"
             mc:Ignorable="d" 
             d:DesignHeight="400" d:DesignWidth="400">
    <Grid>
        <tkx:ChildWindow WindowState="Open" Caption="My Dialog" IsModal="True" WindowStartupLocation="Center">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition />
                </Grid.RowDefinitions>

                <TextBlock Text="{Binding Page3Text, FallbackValue='No Data'}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24" FontWeight="Bold"/>

                <StackPanel Grid.Row="1">
                    <TextBlock Text="Category:" Margin="5"/>
                    <ComboBox Text="Select..." Margin="5">
                        <ComboBoxItem Content="Category 1"/>
                        <ComboBoxItem Content="Category 2"/>
                        <ComboBoxItem Content="Category 3"/>
                    </ComboBox>
                        <TextBlock Text="Items:" Margin="5 10 5 5"/>
                    <ListBox Margin="5" SelectionMode="Multiple">
                        <ListBoxItem Content="Item 1"/>
                        <ListBoxItem Content="Item 2"/>
                        <ListBoxItem Content="Item 3"/>
                        <ListBoxItem Content="Item 4"/>
                        <ListBoxItem Content="Item 5"/>
                        <ListBoxItem Content="Item 6"/>
                        <ListBoxItem Content="Item 7"/>
                        <ListBoxItem Content="Item 8"/>
                    </ListBox>
                </StackPanel>
            </Grid>
        </tkx:ChildWindow>
    </Grid>
</Page>

На этой странице я поместил простенький диалог с выбором элементов из определенной категории. Это пример, приближенный к действительности. Вид диалога был показан на снимке основного окна выше. Обратите внимание, текстовый блок, кажется, лежит в диалоговом окне, но биндится к DataContext’у страницы напрямую, без каждых ухищрений. Это заслуга ChildWindow из WPF Toolkit Extended. Данный контрол на самом деле лишь имитирует поведение диалогового окна и является прямым потомком своего родителя в разметке XAML. Таким образом, DataContext наследуется в ChildWindow от Grid, в тот, что я его разместил.
Коротко о задаче привязки множественного выбора в ListBox. Дабы воротить во ViewModel список выбранных элементов ListBox’a я не могу применять биндинг напрямую, т.к. качество ListBox.SelectedItems не поддерживает биндинг. Дабы решить эту задачу, дозволено отнаследовать от ListBox свой элемент управления, в котором добавить DependencyProperty. Впрочем есть больше эластичный подход в контексте MVVM, о котором я и собираюсь написать в отдельной статье, если Вам это будет увлекательно.

IoC (Инверсия Управления)

К сожалению не могу детально описать данный подход в данной статье. Объем и так крупен. Но Вы можете почерпнуть надобные познания, скажем, из статей на прогре. Также уйма источников дозволено нагуглить. Если коротко, то «Инверсия Управления» это метод устранить прямые ссылки в одной сборке на иную сборку. Инжекция зависимостей выполняется особыми “Контейнерами“, которые из конфигурационных файлов узнают какие определенно классы и из каких сборок инициализировать для указываемого интерфейса и имени сегменты в конфиге. Необходимо сознаться, что в моем коде IoC не реализована всецело. Если Добросовестно, то и цели такой не было. Разумеется, доктрину IoC в коде я попытался отразить и попытался показать каким образом дозволено сделать код менее связным.
Ниже представлены интерфейсы контейнеров и их реализации.

namespace ViewModels.Interfaces
{
    public interface IViewModelsResolver
    {
        INotifyPropertyChanged GetViewModelInstance(string alias);
    }
}

namespace Navigator.Navigation.Interfaces
{
    public interface IPageResolver
    {
        Page GetPageInstance(string alias);
    }
}

Эти интерфейсы играют роль неких контрактов для разных реализаций контейнеров страниц и ViewModel’ей. На данный момент я сделал две реализации, которые Вы ни в коем случае не обязаны применять в реальных планах.

Реализации тестовых контейнеров

namespace Navigator.Navigation
{
    public class PagesResolver : IPageResolver
    {

        private readonly Dictionary<string, Func<Page>> _pagesResolvers = new Dictionary<string, Func<Page>>();

        public PagesResolver()
        {
            _pagesResolvers.Add(Navigation.Page1Alias, () => new Page1());
            _pagesResolvers.Add(Navigation.Page2Alias, () => new Page2());
            _pagesResolvers.Add(Navigation.Page3Alias, () => new Page3());
            _pagesResolvers.Add(Navigation.NotFoundPageAlias, () => new Page404());
        }

        public Page GetPageInstance(string alias)
        {
            if (_pagesResolvers.ContainsKey(alias))
            {
                return _pagesResolvers[alias]();
            }

            return _pagesResolvers[Navigation.NotFoundPageAlias]();
        }
    }
}

namespace ViewModels
{
    public class ViewModelsResolver : IViewModelsResolver
    {

        private readonly Dictionary<string, Func<INotifyPropertyChanged>> _vmResolvers = new Dictionary<string, Func<INotifyPropertyChanged>>();

        public ViewModelsResolver()
        {
            _vmResolvers.Add(MainViewModel.Page1ViewModelAlias, () => new Page1ViewModel());
            _vmResolvers.Add(MainViewModel.Page2ViewModelAlias, () => new Page2ViewModel());
            _vmResolvers.Add(MainViewModel.Page3ViewModelAlias, () => new Page3ViewModel());
            _vmResolvers.Add(MainViewModel.NotFoundPageViewModelAlias, () => new Page404ViewModel());
        }

        public INotifyPropertyChanged GetViewModelInstance(string alias)
        {
            if (_vmResolvers.ContainsKey(alias))
            {
                return _vmResolvers[alias]();
            }

            return _vmResolvers[MainViewModel.NotFoundPageViewModelAlias]();
        }
    }
}

Это легко «куклы» контейнеров, которые подлежать замене на кое-что управляемое, скажем из библиотекиUnity. В качестве интерфейсов тоже отменнее было бы применять что-то подобно IUnityContainer, но мне не хотелось утяжелять солюшен дополнительным референсом и усложнять воспринятие своей реализации навигатора. Тем больше Вы можете выбрать всякую иную библиотеку IoC взамен Unity.

Добавочная письменность

О синглтоне на Википедии
О синглтоне на Програпрогре
О паттерне MVVM на Википедии
О паттерне MVVM на Програпрогре

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB