Сложные приёмы разработки приложений для Windows Phone 8

Основы MVVM

Разбить на страницы
Показывать лекцию целиком

Цель работы: освоить основные понятия MVVM

Основы MVVM

Простейшее приложение для Windows Phone, всеми аспектами которого занимается один разработчик, вполне можно написать, не задумываясь о том, чтобы разделять код, имеющий отношение к визуализации данных, к их обработке и получению из некоторых источников. Приложение, в котором большая часть кода, ответственного за всю его функциональность, собрана в программном файле к его начальной странице (особенно, когда эта функциональность требует пары десятков строк кода), будет работать. Но небольшой рост сложности приложения приводит к тому, что таким кодом становится очень неудобно управлять. Его неудобно расширять, тестировать, неудобно привлекать других программистов к работе над приложением. Небольшое изменение в коде может повлечь за собой множество неожиданных, неприятных последствий. Идея разделения кода, который ответственен за работоспособность различных подсистем приложения, это одна из идей, которая легла в основу создания шаблонов проектирования приложений. Один из таких шаблонов, получивший популярность в последние годы, называется MVVM, Model-View-ViewModel, Модель-Представление-Модель представления. Этот шаблон используют на разных платформах, он универсален и позволяет создавать крупные программные проекты, обладающие чёткой и понятной структурой. Для того чтобы приступить к использованию MVVM, нужно понять сущность некоторых основных понятий, которые имеют к отношение к этому шаблону проектирования.

Очень кратко сущность MVVM можно выразить так: "Модель содержит исходные данные, которые готовятся к выводу с помощью модели представления и отображаются пользователю с помощью представления".

Рассмотрим это более подробно.

Представление (View). Это та часть приложения, которая формирует пользовательский интерфейс и отвечает за взаимодействие с пользователем. К представлению относится XAML-разметка страницы, отображаемые на странице элементы управления, анимации, которые применяются к элементам управления и логика, которая имеет отношение к интерфейсу. В идеальном варианте представление – это исключительно XAML-код, который описывает страницу и элементы управления, а в файле программного кода к этой странице содержится лишь конструктор, который вызывается при создании страницы. Для отображения интерфейса, однако, часто требуются какие-либо специфические операции, которые так же реализуют в файлах кода страниц. Представление, конечно, не используется автономно, никому не нужен интерфейс, который не отображает полезных данных и не способен обеспечить взаимодействие пользователя с этими данными. В том, что касается визуализации каких-либо списков данных, а подобные интерфейсы весьма популярны, можно отметить использование шаблонов вывода данных. Шаблон может быть использован множество раз для форматирования списков данных переменной длины, выводимых в интерфейс. Взаимодействие представления с логикой приложения, которая находится на более "глубоких" уровнях MVVM, в частности – с моделью представления, обеспечивают механизмы привязки данных и команд.

Привязка данных. Привязка данных концептуально выглядит следующим образом. Имеется некий элемент управления, например – текстовое поле, в который мы хотим вывести некоторые данные. В XAML-коде, описывающем данное поле мы настраиваем связь этого поля с нужным нам свойством некоего объекта, который содержит данные, которые надо вывести в поле. Текстовое поле называют целевым объектом привязки или приемником, объект, содержащий данные, называют источником данных. Существуют разные типы привязки данных. В частности, так называемая односторонняя (one way) привязка данных приводит к тому, что информация берется из источника, направляется в приемник, при этом, если в приемнике происходит изменение данных, эти изменения не влияют на источник. Но если информация изменилась в источнике данных, изменения отразятся и на приемнике. При двусторонней привязке (two way) изменения, которым данные подвергнуться в приемнике, отражаются и на источнике данных. При этом описание привязок данных осуществляется в XAML-коде страницы, то есть, программный код для реализации этого механизма не нужен. Существуют и другие типы привязки, например, одноразовая (One time) – данные из источника передаются в приемник один раз, например, при смене контекста данных или при загрузке приложения, затем связь между ними не поддерживается. Передачу данных между источником и приемником выполняют механизмы системы привязки данных. К привязке данных относится такое понятие как конвертер данных – программный механизм, которые выполняет преобразование данных.

Привязка команд. Если из пользовательского интерфейса нужно произвести какое-либо управляющее воздействие, эту операцию реализуют с помощью привязки команд. Код команд определен в модели представления, а вызываются они из пользовательского интерфейса. В идеале, как и в случае с привязкой данных, привязка команд осуществляется в XAML-коде.

Модель представления. Модель представления связывает исходные данные, представленные моделью, и их визуальное представление. При этом модель представления не обладает "знаниями" о том, как именно данные используются в представлении – то есть – в интерфейсе приложения. Задача модели представления заключается в подготовке данных к отображению и в предоставлении этих данных представлению, то есть – интерфейсным механизмам – через систему привязки данных. То же самое касается команд. В модели представления может быть определен некий метод, который вызывается каким-либо элементом управления из представления, но этот метод не имеет информации о том, какой именно элемент управления его вызывает. Благодаря подобному разделению представления (интерфейса) и логики представления (модели представления) и достигается гибкость работы над различными частями приложения, созданного по методологии MVVM.

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

При проектировании приложений с использованием шаблона MVVM очень важно правильно распределить функциональность приложения между его моделью, представлением и моделью представления.

Если рассматривать реализацию MVVM-приложения с использованием стандартных средств, доступных на платформе Windows Phone, то здесь можно отметить следующие особенности.

  • Классы моделей и моделей представления обычно реализуют интерфейс INotifyPropertyChanged (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.componentmodel.inotifypropertychanged%28v=vs.105%29.aspx). Благодаря его использованию обеспечивается работоспособность системы привязки данных. Сущность его использования заключается в том, что его реализация позволяет отправлять уведомления с помощью события PropertyChanged (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.componentmodel.inotifypropertychanged.propertychanged%28v=vs.105%29.aspx), сигнализирующие об изменении данных приложения. Эти изменения, благодаря системе привязки данных, в итоге, "доходят" от модели (то есть – от уровня исходных данных, на которых основано приложение), через модель представления к пользовательскому интерфейсу. Нужно отметить, что некоторые типы, например ObservableCollection<T> (http://msdn.microsoft.com/en-us/library/windowsphone/develop/ms668604%28v=vs.105%29.aspx), представляющий динамическую коллекцию, подходящую для привязки данных, так же предусматривают реализацию INotifyPropertyChanged.
  • Классы моделей, если им нужно снабжать другие классы информацией об ошибках в данных, выполнять проверку данных и сообщать о её результатах, реализуют интерфейс INotifyDataErrorInfo (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.componentmodel.inotifydataerrorinfo%28v=vs.105%29.aspx).
  • Привязка данных может осуществляться как в XAML-разметке, так и в программном коде. Первый метод предпочтительнее, так как он, в идеале, позволяет создавать пользовательские интерфейсы, всё описание которых сосредоточено в XAML-файлах, файлы кода подобных интерфейсов содержат лишь конструкторы. В привязке данных существует понятие контекст данных (data context) он определяет объект, к которому осуществляется привязка. Свойство DataContext (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.windows.frameworkelement.datacontext%28v=vs.105%29.aspx) класса FrameworkElement (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.windows.frameworkelement%28v=vs.105%29.aspx) позволяет определять контекст данных для элементов интерфейса, в иерархии наследования которых есть FrameworkElement. Именно этот класс ответственен за возможность элементов пользовательского интерфейса участвовать в операциях привязки данных. Когда определен контекст данных, в XAML привязка осуществляется с помощью расширения разметки {Binding ...}. Здесь используется класс Binding (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.windows.data.binding%28v=vs.105%29.aspx). К этому же классу относятся и конвертеры значений.
  • Привязка команд осуществляется благодаря свойству элементов управления Command, она, так же как и привязка данных, может быть описана в XAML. При описании команд реализуют интерфейс ICommand (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.windows.input.icommand%28v=vs.105%29.aspx).
  • Так как модель (данные), модель представления (подготовка данных) и представление (отображение данных в интерфейсе) разделены, при использовании шаблона MVVM особую значимость приобретает концепция использования так называемых данных периода проектирования (design-time data). Эти данные обычно представлены в некотором статичном виде, например – в файле, содержащем заранее заданный набор данных. Использование данных периода проектирования позволяет заниматься проектированием интерфейса приложения в виде, максимально приближенном к рабочему виду приложения.
  • Рассмотрим реализацию некоторых вышеописанных принципов в приложении, построенном по шаблону Приложение Windows Phone с привязкой к данным.

    Шаблон Приложение Windows Phone с привязкой к данным

    Приложение, построенное с использованием данного шаблона, реализует основные принципы, характерные для шаблона MVVM. Нельзя сказать, что такое приложение в полной мере соответствует принципам MVVM. Например, модель и модель представления разделены недостаточно чётко (хотя, в данном случае можно говорить о том, что файл ItemViewModel, представляющий собой класс, определяющий структуру данных, которая используется в приложении, ближе к понятию "Model", чем к понятию "ViewModel", которое присутствует в его названии). В файлах программного кода страниц, которые формируют пользовательский интерфейс, производятся некоторые действия. Однако, есть в этом шаблоне и реализация основополагающих идей MVVM. В частности, он предусматривает использование данных периода проектирования, связь между исходными данными и представлением реализована с помощью системы привязки данных. В интерфейсе используются шаблоны, что закладывает основу для работы со списками данных переменной длины.

    Рассмотрим приложение, созданное по этому шаблону, подробнее. На рис. 21.1. приведено окно Visual Studio, в котором развёрнута структура проекта, в визуальном конструкторе открыта страница MainPage.xaml.

    (рис 21.1) Приложение, созданное по шаблону Приложение Windows Phone с привязкой к данным

    Начнём рассмотрение проекта с папки SampleData. Здесь находится XAML-файл (Листинг 21.1), MainViewModelSampleData, который содержит данные периода проектирования. Структура данных, которые описаны в файле определяется классом ItemViewModel. В заголовке XAML-файла описано пространство имен vm, соответствующее пространству имен класса ItemViewModel. В файле содержится, кроме того, описание одиночного свойства из класса MainViewModel.

    <vm:MainViewModel
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:L5_1.ViewModels"
        SampleProperty="Пример значения свойства текста">
    
        <vm:MainViewModel.Items>
            <vm:ItemViewModel ID="0" LineOne="первая схема" 
                                 LineTwo="Maecenas praesent accumsan bibendum" 
                     LineThree="Maecenas praesent accumsan bibendum 
                                dictumst eleifend facilisi faucibus 
                          habitant inceptos interdum lobortis 
                          nascetur"/>
            <vm:ItemViewModel ID="1" LineOne="вторая схема" 
                                 LineTwo="Dictumst eleifend facilisi faucibus" 
                     LineThree="Pharetra placerat pulvinar sagittis 
                                senectus sociosqu suscipit torquent 
                          ultrices vehicula volutpat maecenas 
                          praesent"/>
    
        </vm:MainViewModel.Items>
    </vm:MainViewModel>
    

    Этот файл и представляет собой тот контекст данных, который назначен страницам приложения. При этом в режиме проектирования контекст данных для страницы MainPage (Листинг 21.2) соответствует общему списку данных, на этой странице выводится список элементов. А на странице DetailsPage (Листинг 21.3) выводится подробная информации об элементе с главной страницы, которого, при работе приложения, коснулся пользователь. В режиме проектирования контекст данных страницы так же установлен на вышеупомянутый файл, но контекст данных сетки LayoutRoot уточнён до элемента с индексом [0].

    <phone:PhoneApplicationPage
        x:Class="L5_1.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
        xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        d:DataContext="{d:DesignData SampleData/MainViewModelSampleData.xaml}"
        FontFamily="{StaticResource PhoneFontFamilyNormal}"
        FontSize="{StaticResource PhoneFontSizeNormal}"
        Foreground="{StaticResource PhoneForegroundBrush}"
        SupportedOrientations="Portrait"  Orientation="Portrait"
        shell:SystemTray.IsVisible="True">
    
        <!--Для контекста данных установлен верхний пример данных, и LayoutRoot содержит корневую сетку, где размещается все содержимое страницы-->
        <Grid x:Name="LayoutRoot" Background="Transparent">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
    
           <!--TitlePanel содержит имя приложения и заголовок страницы-->
            <StackPanel Grid.Row="0" Margin="12,17,0,28">
                <TextBlock Text="МОЕ ПРИЛОЖЕНИЕ" Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBlock Text="имя страницы" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
            </StackPanel>
    
            <!--ContentPanel содержит LongListSelector и LongListSelector ItemTemplate. Поместите здесь дополнительное содержимое-->
            <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
                <phone:LongListSelector x:Name="MainLongListSelector" Margin="0,0,-12,0" ItemsSource="{Binding Items}" 
                SelectionChanged="MainLongListSelector_SelectionChanged">
                    <phone:LongListSelector.ItemTemplate>
                        <DataTemplate>
                          <StackPanel Margin="0,0,0,17">
                              <TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                              <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" 
                              Style="{StaticResource PhoneTextSubtleStyle}"/>
                          </StackPanel>
                        </DataTemplate>
                    </phone:LongListSelector.ItemTemplate>
                </phone:LongListSelector>
            </Grid>
    
        </Grid>
    
    </phone:PhoneApplicationPage>
    

    Класс ItemViewModel, по сути, ближе всего к понятию "модели", принятом в MVVM. Он описывает базовую структуру данных, применяемых в программе. Эти данные применяются, во-первых, при заполнении страницы MainPage – на ней выводится список элементов, во-вторых – при заполнении страницы DetailsPage, которая содержит подробное описание одного элемента. Как уже было сказано, в режиме проектирования данные, выводимые на страницах, определены в файле MainViewModelSampleData.xaml. При запуске приложения контекст данных страницы MainPage (Листинг 21.4) устанавливается на объект типа MainViewModel, созданный в классе App (Листинг 21.5). Похожая процедура выполняется и при вызове страницы DetailsPage (Листинг 21.6). В XAML контекст данных установлен на один из элементов, описанных в MainViewModelSampleData.xaml, в режиме исполнения он программно устанавливается на один из элементов, присутствующих в объекте типа MainViewModel, который создаётся в классе App.

    <phone:PhoneApplicationPage
        x:Class="L5_1.DetailsPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
        xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        d:DataContext="{d:DesignData SampleData/MainViewModelSampleData.xaml}"
        mc:Ignorable="d"
        FontFamily="{StaticResource PhoneFontFamilyNormal}"
        FontSize="{StaticResource PhoneFontSizeNormal}"
        Foreground="{StaticResource PhoneForegroundBrush}"
        SupportedOrientations="Portrait"  Orientation="Portrait"
        shell:SystemTray.IsVisible="True">
    
        <!--Для контекста данных установлен верхний пример данных и первый элемент из коллекции примеров данных ниже,
         и LayoutRoot содержит корневую сетку, где размещается все содержимое страницы-->
        <Grid x:Name="LayoutRoot" Background="Transparent" d:DataContext="{Binding Items[0]}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
    
            <!--TitlePanel содержит имя приложения и заголовок страницы-->
            <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
                <TextBlock Text="МОЕ ПРИЛОЖЕНИЕ" Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBlock Text="{Binding LineOne}" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
            </StackPanel>
    
            <!--ContentPanel содержит текст сведений. Поместите здесь дополнительное содержимое-->
            <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
                <TextBlock Text="{Binding LineThree}" TextWrapping="Wrap" Style="{StaticResource PhoneTextNormalStyle}"/>
            </Grid>
        </Grid>
    
    </phone:PhoneApplicationPage>
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Navigation;
    using Microsoft.Phone.Controls;
    using Microsoft.Phone.Shell;
    using L5_1.Resources;
    using L5_1.ViewModels;
    
    namespace L5_1
    {
        public partial class MainPage : PhoneApplicationPage
        {
            // Конструктор
            public MainPage()
            {
                InitializeComponent();
    
                // Задайте для контекста данных элемента управления LongListSelector пример данных
                DataContext = App.ViewModel;
    
            }
    
            // Загрузка данных для элементов ViewModel
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                if (!App.ViewModel.IsDataLoaded)
                {
                    App.ViewModel.LoadData();
                }
            }
    
            // Обработка выбранных элементов, измененных в LongListSelector
            private void MainLongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // Если выбранный элемент равен NULL (ничего не выбрано), никакие действия не требуются
                if (MainLongListSelector.SelectedItem == null)
                    return;
    
                // Переход на новую страницу
                NavigationService.Navigate(new Uri("/DetailsPage.xaml?selectedItem=" +
                 (MainLongListSelector.SelectedItem as ItemViewModel).ID, UriKind.Relative));
    
                // Сброс выбранного элемента в NULL (ничего не выбрано)
                MainLongListSelector.SelectedItem = null;
            }
      
        }
    }
    
    using System;
    using System.Diagnostics;
    using System.Resources;
    using System.Windows;
    using System.Windows.Markup;
    using System.Windows.Navigation;
    using Microsoft.Phone.Controls;
    using Microsoft.Phone.Shell;
    using L5_1.Resources;
    using L5_1.ViewModels;
    
    namespace L5_1
    {
        public partial class App : Application
        {
            private static MainViewModel viewModel = null;
    
            /// <summary>
            /// Статический элемент ViewModel, используемый в представлениях для привязки.
            /// </summary>
            /// <returns>Объект MainViewModel.</returns>
            public static MainViewModel ViewModel
            {
                get
                {
                    // Отложить создание модели представления до необходимости
                    if (viewModel == null)
                        viewModel = new MainViewModel();
    
                    return viewModel;
                }
            }
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Navigation;
    using Microsoft.Phone.Controls;
    using Microsoft.Phone.Shell;
    using L5_1.Resources;
    
    namespace L5_1
    {
        public partial class DetailsPage : PhoneApplicationPage
        {
            // Конструктор
            public DetailsPage()
            {
                InitializeComponent();
    
            }
    
            // При переходе на страницу установите для контекста данных выбранный элемент в списке
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                if (DataContext == null)
                {
                    string selectedIndex = "";
                    if (NavigationContext.QueryString.TryGetValue("selectedItem", out selectedIndex))
                    {
                        int index = int.Parse(selectedIndex);
                        DataContext = App.ViewModel.Items[index];
                    }
                }
            }
        }
    }
    

    Если говорить о классе ItemViewModel (Листинг 21.7), то он не связан с представлениями, он реализует интерфейс INotifyPropertyChanged, событие PropertyChanged вызывается при изменении свойств, которые описаны в классе.

    using System;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    
    namespace L5_1.ViewModels
    {
        public class ItemViewModel : INotifyPropertyChanged
        {
            private string _id;
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется для идентификации объекта.
            /// </summary>
            /// <returns></returns>
            public string ID
            {
                get
                {
                    return _id;
                }
                set
                {
                    if (value != _id)
                    {
                        _id = value;
                        NotifyPropertyChanged("ID");
                    }
                }
            }
    
            private string _lineOne;
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется в представлении для отображения его значения с помощью привязки.
            /// </summary>
            /// <returns></returns>
            public string LineOne
            {
                get
                {
                    return _lineOne;
                }
                set
                {
                    if (value != _lineOne)
                    {
                        _lineOne = value;
                        NotifyPropertyChanged("LineOne");
                    }
                }
            }
    
            private string _lineTwo;
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется в представлении для отображения его значения с помощью привязки.
            /// </summary>
            /// <returns></returns>
            public string LineTwo
            {
                get
                {
                    return _lineTwo;
                }
                set
                {
                    if (value != _lineTwo)
                    {
                        _lineTwo = value;
                        NotifyPropertyChanged("LineTwo");
                    }
                }
            }
    
            private string _lineThree;
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется в представлении для отображения его значения с помощью привязки.
            /// </summary>
            /// <returns></returns>
            public string LineThree
            {
                get
                {
                    return _lineThree;
                }
                set
                {
                    if (value != _lineThree)
                    {
                        _lineThree = value;
                        NotifyPropertyChanged("LineThree");
                    }
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
            private void NotifyPropertyChanged(String propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (null != handler)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }
    

    В классе MainViewModel так же реализован INotifyPropertyChanged. Здесь событие PropertyChanged генерируется при изменении свойства SampleProperty. При этом класс содержит открытую коллекцию Items типа ItemViewModel – именно элементы этой коллекции, которая заполняется в этом же классе, и представляют собой те данные, которые при выполнении программы выводятся на главной странице в виде списка, и она же поставляет данные для страницы просмотра подробных сведений об элементе.

    using System;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using L5_1.Resources;
    
    namespace L5_1.ViewModels
    {
        public class MainViewModel : INotifyPropertyChanged
        {
            public MainViewModel()
            {
                this.Items = new ObservableCollection<ItemViewModel>();
            }
    
            /// <summary>
            /// Коллекция объектов ItemViewModel.
            /// </summary>
            public ObservableCollection<ItemViewModel> Items { get; private set; }
    
            private string _sampleProperty = "Sample Runtime Property Value";
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется в представлении 
    для отображения его значения с помощью привязки
            /// </summary>
            /// <returns></returns>
            public string SampleProperty
            {
                get
                {
                    return _sampleProperty;
                }
                set
                {
                    if (value != _sampleProperty)
                    {
                        _sampleProperty = value;
                        NotifyPropertyChanged("SampleProperty");
                    }
                }
            }
    
            /// <summary>
            /// Пример свойства, возвращающего локализованную строку
            /// </summary>
            public string LocalizedSampleProperty
            {
                get
                {
                    return AppResources.SampleProperty;
                }
            }
    
            public bool IsDataLoaded
            {
                get;
                private set;
            }
    
            /// <summary>
            /// Создает и добавляет несколько объектов ItemViewModel в коллекцию элементов.
            /// </summary>
            public void LoadData()
            {
                // Пример данных; замените реальными данными
                this.Items.Add(new ItemViewModel() { ID = "0", LineOne = "runtime one", 
                LineTwo = "Maecenas praesent accumsan bibendum", 
                LineThree = "Facilisi faucibus habitant inceptos interdum lobortis 
    nascetur pharetra placerat pulvinar sagittis senectus sociosqu" });
                this.Items.Add(new ItemViewModel() { ID = "1", LineOne = "runtime two", 
                LineTwo = "Dictumst eleifend facilisi faucibus", 
                LineThree = "Suscipit torquent ultrices vehicula volutpat maecenas praesent accumsan
     bibendum dictumst eleifend facilisi faucibus" });
                this.IsDataLoaded = true;
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
            private void NotifyPropertyChanged(String propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (null != handler)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }
    

    В результате использования в приложении, построенном по шаблону Приложение с привязкой данных, шаблона MVVM, мы, по сравнению с приложением, построенном, например, по шаблону Приложение для Windows Phone получаем некоторые преимущества. А именно – код – работающий с данными и презентационная логика приложения разделены. При создании интерфейса приложения мы получаем возможность работать с данными периода проектирования, которые позволяют при проектировании отобразить интерфейс, основанный на шаблоне (в частности, на странице MainPage) в виде, максимально приближенном к виду работающего приложения. И хотя этот шаблон не вполне соответствует принципам MVVM, использование подходов, примененных в нём, в проектах, которые предусматривают работу с некоторыми наборами данных значительно облегчает создание подобных проектов. Кроме того, освоение этого проекта позволяет переходить к освоению других инструментов и примеров приложений.

    Нужно отметить, что приложения, построенные по шаблонам Приложение Windows Phone с панорамой и Приложение Windows Phone Pivot используют подход, похожий на тот, который применяется в шаблоне Приложение Windows Phone с привязкой к данным.

    Материалы для дальнейшего изучения

    В качестве наглядной иллюстрации реализации концепции MVVM в приложениях для Windows Phone, можно привести пример "Model-View-ViewModel" (http://code.msdn.microsoft.com/wpapps/Model-View-ViewModel-Sample-8cb92fd9). Его можно либо загрузить по вышеуказанной ссылке, либо, в Visual Studio, в окне Создать проект перейти в группу В Интернете > Примеры. Здесь, в строке поиска, нужно ввести MVVM, что приведет к нахождению нескольких примеров, среди которых нас интересует "Model-View-ViewModel" (рис. 21.2), в данном случае – для Visual C#, тот же пример доступен и для Visual Basic.

    (рис 21.2) Результаты поиска по примерам в окне Создать проект

    Пример можно выбрать и создать с его помощью проект. Кроме того, для ознакомления с данным примером можно воспользоваться пошаговым руководством "Реализация шаблона MVVM в приложении для Windows Phone" (http://msdn.microsoft.com/en-us/library/windowsphone/develop/gg521153%28v=vs.105%29.aspx).

    Для построения более совершенных, с точки зрения реализации MVVM, проектов, можно воспользоваться специальными наборами инструментов. Например, MVVM Light Toolkit (http://mvvmlight.codeplex.com/releases). На указанной странице проекта можно загрузить его установочный комплект, после установки которого и установки соответствующего VSIX-файла (инструкции по установке отображаются после инсталляции пакета) в списке C#-шаблонов приложений для Windows Phone появляется новый шаблон. MVVM Light доступен и через службу NuGet (Сервис > Диспетчер библиотек > Управление пакетами NuGet для решения).

    Существуют и другие наборы инструментов, облегчающие и ускоряющие разработку с использованием шаблона проектирования MVVM.

    Выводы

    В этой лекции мы рассмотрели основные понятия шаблона проектирования приложений MVVM, ознакомились с понятиями модели, представления, модели представления, привязки данных и команд. Применение данного шаблона позволяет упростить разработку сложных программных проектов за счёт чёткого разделения подсистем работы с данными и подсистем пользовательского интерфейса.

    Задание

    Проанализируйте идею приложения, которое вы разрабатываете, с точки зрения применения при его разработке шаблона проектирования MVVM. Разбейте логику приложения на модель, представление и модель представления, подготовьте отчёт о проделанной работе.

    Дополнительные материалы

    К данной лекции подготовлено видеоприложение и демонстрационный программный проект.

    Страницы:

    Цель работы: освоить основные понятия MVVM

    Основы MVVM

    Простейшее приложение для Windows Phone, всеми аспектами которого занимается один разработчик, вполне можно написать, не задумываясь о том, чтобы разделять код, имеющий отношение к визуализации данных, к их обработке и получению из некоторых источников. Приложение, в котором большая часть кода, ответственного за всю его функциональность, собрана в программном файле к его начальной странице (особенно, когда эта функциональность требует пары десятков строк кода), будет работать. Но небольшой рост сложности приложения приводит к тому, что таким кодом становится очень неудобно управлять. Его неудобно расширять, тестировать, неудобно привлекать других программистов к работе над приложением. Небольшое изменение в коде может повлечь за собой множество неожиданных, неприятных последствий. Идея разделения кода, который ответственен за работоспособность различных подсистем приложения, это одна из идей, которая легла в основу создания шаблонов проектирования приложений. Один из таких шаблонов, получивший популярность в последние годы, называется MVVM, Model-View-ViewModel, Модель-Представление-Модель представления. Этот шаблон используют на разных платформах, он универсален и позволяет создавать крупные программные проекты, обладающие чёткой и понятной структурой. Для того чтобы приступить к использованию MVVM, нужно понять сущность некоторых основных понятий, которые имеют к отношение к этому шаблону проектирования.

    Очень кратко сущность MVVM можно выразить так: "Модель содержит исходные данные, которые готовятся к выводу с помощью модели представления и отображаются пользователю с помощью представления".

    Рассмотрим это более подробно.

    Представление (View). Это та часть приложения, которая формирует пользовательский интерфейс и отвечает за взаимодействие с пользователем. К представлению относится XAML-разметка страницы, отображаемые на странице элементы управления, анимации, которые применяются к элементам управления и логика, которая имеет отношение к интерфейсу. В идеальном варианте представление – это исключительно XAML-код, который описывает страницу и элементы управления, а в файле программного кода к этой странице содержится лишь конструктор, который вызывается при создании страницы. Для отображения интерфейса, однако, часто требуются какие-либо специфические операции, которые так же реализуют в файлах кода страниц. Представление, конечно, не используется автономно, никому не нужен интерфейс, который не отображает полезных данных и не способен обеспечить взаимодействие пользователя с этими данными. В том, что касается визуализации каких-либо списков данных, а подобные интерфейсы весьма популярны, можно отметить использование шаблонов вывода данных. Шаблон может быть использован множество раз для форматирования списков данных переменной длины, выводимых в интерфейс. Взаимодействие представления с логикой приложения, которая находится на более "глубоких" уровнях MVVM, в частности – с моделью представления, обеспечивают механизмы привязки данных и команд.

    Привязка данных. Привязка данных концептуально выглядит следующим образом. Имеется некий элемент управления, например – текстовое поле, в который мы хотим вывести некоторые данные. В XAML-коде, описывающем данное поле мы настраиваем связь этого поля с нужным нам свойством некоего объекта, который содержит данные, которые надо вывести в поле. Текстовое поле называют целевым объектом привязки или приемником, объект, содержащий данные, называют источником данных. Существуют разные типы привязки данных. В частности, так называемая односторонняя (one way) привязка данных приводит к тому, что информация берется из источника, направляется в приемник, при этом, если в приемнике происходит изменение данных, эти изменения не влияют на источник. Но если информация изменилась в источнике данных, изменения отразятся и на приемнике. При двусторонней привязке (two way) изменения, которым данные подвергнуться в приемнике, отражаются и на источнике данных. При этом описание привязок данных осуществляется в XAML-коде страницы, то есть, программный код для реализации этого механизма не нужен. Существуют и другие типы привязки, например, одноразовая (One time) – данные из источника передаются в приемник один раз, например, при смене контекста данных или при загрузке приложения, затем связь между ними не поддерживается. Передачу данных между источником и приемником выполняют механизмы системы привязки данных. К привязке данных относится такое понятие как конвертер данных – программный механизм, которые выполняет преобразование данных.

    Привязка команд. Если из пользовательского интерфейса нужно произвести какое-либо управляющее воздействие, эту операцию реализуют с помощью привязки команд. Код команд определен в модели представления, а вызываются они из пользовательского интерфейса. В идеале, как и в случае с привязкой данных, привязка команд осуществляется в XAML-коде.

    Модель представления. Модель представления связывает исходные данные, представленные моделью, и их визуальное представление. При этом модель представления не обладает "знаниями" о том, как именно данные используются в представлении – то есть – в интерфейсе приложения. Задача модели представления заключается в подготовке данных к отображению и в предоставлении этих данных представлению, то есть – интерфейсным механизмам – через систему привязки данных. То же самое касается команд. В модели представления может быть определен некий метод, который вызывается каким-либо элементом управления из представления, но этот метод не имеет информации о том, какой именно элемент управления его вызывает. Благодаря подобному разделению представления (интерфейса) и логики представления (модели представления) и достигается гибкость работы над различными частями приложения, созданного по методологии MVVM.

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

    При проектировании приложений с использованием шаблона MVVM очень важно правильно распределить функциональность приложения между его моделью, представлением и моделью представления.

    Если рассматривать реализацию MVVM-приложения с использованием стандартных средств, доступных на платформе Windows Phone, то здесь можно отметить следующие особенности.

  • Классы моделей и моделей представления обычно реализуют интерфейс INotifyPropertyChanged (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.componentmodel.inotifypropertychanged%28v=vs.105%29.aspx). Благодаря его использованию обеспечивается работоспособность системы привязки данных. Сущность его использования заключается в том, что его реализация позволяет отправлять уведомления с помощью события PropertyChanged (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.componentmodel.inotifypropertychanged.propertychanged%28v=vs.105%29.aspx), сигнализирующие об изменении данных приложения. Эти изменения, благодаря системе привязки данных, в итоге, "доходят" от модели (то есть – от уровня исходных данных, на которых основано приложение), через модель представления к пользовательскому интерфейсу. Нужно отметить, что некоторые типы, например ObservableCollection<T> (http://msdn.microsoft.com/en-us/library/windowsphone/develop/ms668604%28v=vs.105%29.aspx), представляющий динамическую коллекцию, подходящую для привязки данных, так же предусматривают реализацию INotifyPropertyChanged.
  • Классы моделей, если им нужно снабжать другие классы информацией об ошибках в данных, выполнять проверку данных и сообщать о её результатах, реализуют интерфейс INotifyDataErrorInfo (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.componentmodel.inotifydataerrorinfo%28v=vs.105%29.aspx).
  • Привязка данных может осуществляться как в XAML-разметке, так и в программном коде. Первый метод предпочтительнее, так как он, в идеале, позволяет создавать пользовательские интерфейсы, всё описание которых сосредоточено в XAML-файлах, файлы кода подобных интерфейсов содержат лишь конструкторы. В привязке данных существует понятие контекст данных (data context) он определяет объект, к которому осуществляется привязка. Свойство DataContext (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.windows.frameworkelement.datacontext%28v=vs.105%29.aspx) класса FrameworkElement (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.windows.frameworkelement%28v=vs.105%29.aspx) позволяет определять контекст данных для элементов интерфейса, в иерархии наследования которых есть FrameworkElement. Именно этот класс ответственен за возможность элементов пользовательского интерфейса участвовать в операциях привязки данных. Когда определен контекст данных, в XAML привязка осуществляется с помощью расширения разметки {Binding ...}. Здесь используется класс Binding (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.windows.data.binding%28v=vs.105%29.aspx). К этому же классу относятся и конвертеры значений.
  • Привязка команд осуществляется благодаря свойству элементов управления Command, она, так же как и привязка данных, может быть описана в XAML. При описании команд реализуют интерфейс ICommand (http://msdn.microsoft.com/en-us/library/windowsphone/develop/system.windows.input.icommand%28v=vs.105%29.aspx).
  • Так как модель (данные), модель представления (подготовка данных) и представление (отображение данных в интерфейсе) разделены, при использовании шаблона MVVM особую значимость приобретает концепция использования так называемых данных периода проектирования (design-time data). Эти данные обычно представлены в некотором статичном виде, например – в файле, содержащем заранее заданный набор данных. Использование данных периода проектирования позволяет заниматься проектированием интерфейса приложения в виде, максимально приближенном к рабочему виду приложения.
  • Рассмотрим реализацию некоторых вышеописанных принципов в приложении, построенном по шаблону Приложение Windows Phone с привязкой к данным.

    Шаблон Приложение Windows Phone с привязкой к данным

    Приложение, построенное с использованием данного шаблона, реализует основные принципы, характерные для шаблона MVVM. Нельзя сказать, что такое приложение в полной мере соответствует принципам MVVM. Например, модель и модель представления разделены недостаточно чётко (хотя, в данном случае можно говорить о том, что файл ItemViewModel, представляющий собой класс, определяющий структуру данных, которая используется в приложении, ближе к понятию "Model", чем к понятию "ViewModel", которое присутствует в его названии). В файлах программного кода страниц, которые формируют пользовательский интерфейс, производятся некоторые действия. Однако, есть в этом шаблоне и реализация основополагающих идей MVVM. В частности, он предусматривает использование данных периода проектирования, связь между исходными данными и представлением реализована с помощью системы привязки данных. В интерфейсе используются шаблоны, что закладывает основу для работы со списками данных переменной длины.

    Рассмотрим приложение, созданное по этому шаблону, подробнее. На рис. 21.1. приведено окно Visual Studio, в котором развёрнута структура проекта, в визуальном конструкторе открыта страница MainPage.xaml.

    (рис 21.1) Приложение, созданное по шаблону Приложение Windows Phone с привязкой к данным

    Начнём рассмотрение проекта с папки SampleData. Здесь находится XAML-файл (Листинг 21.1), MainViewModelSampleData, который содержит данные периода проектирования. Структура данных, которые описаны в файле определяется классом ItemViewModel. В заголовке XAML-файла описано пространство имен vm, соответствующее пространству имен класса ItemViewModel. В файле содержится, кроме того, описание одиночного свойства из класса MainViewModel.

    <vm:MainViewModel
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:L5_1.ViewModels"
        SampleProperty="Пример значения свойства текста">
    
        <vm:MainViewModel.Items>
            <vm:ItemViewModel ID="0" LineOne="первая схема" 
                                 LineTwo="Maecenas praesent accumsan bibendum" 
                     LineThree="Maecenas praesent accumsan bibendum 
                                dictumst eleifend facilisi faucibus 
                          habitant inceptos interdum lobortis 
                          nascetur"/>
            <vm:ItemViewModel ID="1" LineOne="вторая схема" 
                                 LineTwo="Dictumst eleifend facilisi faucibus" 
                     LineThree="Pharetra placerat pulvinar sagittis 
                                senectus sociosqu suscipit torquent 
                          ultrices vehicula volutpat maecenas 
                          praesent"/>
    
        </vm:MainViewModel.Items>
    </vm:MainViewModel>
    

    Этот файл и представляет собой тот контекст данных, который назначен страницам приложения. При этом в режиме проектирования контекст данных для страницы MainPage (Листинг 21.2) соответствует общему списку данных, на этой странице выводится список элементов. А на странице DetailsPage (Листинг 21.3) выводится подробная информации об элементе с главной страницы, которого, при работе приложения, коснулся пользователь. В режиме проектирования контекст данных страницы так же установлен на вышеупомянутый файл, но контекст данных сетки LayoutRoot уточнён до элемента с индексом [0].

    <phone:PhoneApplicationPage
        x:Class="L5_1.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
        xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        d:DataContext="{d:DesignData SampleData/MainViewModelSampleData.xaml}"
        FontFamily="{StaticResource PhoneFontFamilyNormal}"
        FontSize="{StaticResource PhoneFontSizeNormal}"
        Foreground="{StaticResource PhoneForegroundBrush}"
        SupportedOrientations="Portrait"  Orientation="Portrait"
        shell:SystemTray.IsVisible="True">
    
        <!--Для контекста данных установлен верхний пример данных, и LayoutRoot содержит корневую сетку, где размещается все содержимое страницы-->
        <Grid x:Name="LayoutRoot" Background="Transparent">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
    
           <!--TitlePanel содержит имя приложения и заголовок страницы-->
            <StackPanel Grid.Row="0" Margin="12,17,0,28">
                <TextBlock Text="МОЕ ПРИЛОЖЕНИЕ" Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBlock Text="имя страницы" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
            </StackPanel>
    
            <!--ContentPanel содержит LongListSelector и LongListSelector ItemTemplate. Поместите здесь дополнительное содержимое-->
            <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
                <phone:LongListSelector x:Name="MainLongListSelector" Margin="0,0,-12,0" ItemsSource="{Binding Items}" 
                SelectionChanged="MainLongListSelector_SelectionChanged">
                    <phone:LongListSelector.ItemTemplate>
                        <DataTemplate>
                          <StackPanel Margin="0,0,0,17">
                              <TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                              <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" 
                              Style="{StaticResource PhoneTextSubtleStyle}"/>
                          </StackPanel>
                        </DataTemplate>
                    </phone:LongListSelector.ItemTemplate>
                </phone:LongListSelector>
            </Grid>
    
        </Grid>
    
    </phone:PhoneApplicationPage>
    

    Класс ItemViewModel, по сути, ближе всего к понятию "модели", принятом в MVVM. Он описывает базовую структуру данных, применяемых в программе. Эти данные применяются, во-первых, при заполнении страницы MainPage – на ней выводится список элементов, во-вторых – при заполнении страницы DetailsPage, которая содержит подробное описание одного элемента. Как уже было сказано, в режиме проектирования данные, выводимые на страницах, определены в файле MainViewModelSampleData.xaml. При запуске приложения контекст данных страницы MainPage (Листинг 21.4) устанавливается на объект типа MainViewModel, созданный в классе App (Листинг 21.5). Похожая процедура выполняется и при вызове страницы DetailsPage (Листинг 21.6). В XAML контекст данных установлен на один из элементов, описанных в MainViewModelSampleData.xaml, в режиме исполнения он программно устанавливается на один из элементов, присутствующих в объекте типа MainViewModel, который создаётся в классе App.

    <phone:PhoneApplicationPage
        x:Class="L5_1.DetailsPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
        xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        d:DataContext="{d:DesignData SampleData/MainViewModelSampleData.xaml}"
        mc:Ignorable="d"
        FontFamily="{StaticResource PhoneFontFamilyNormal}"
        FontSize="{StaticResource PhoneFontSizeNormal}"
        Foreground="{StaticResource PhoneForegroundBrush}"
        SupportedOrientations="Portrait"  Orientation="Portrait"
        shell:SystemTray.IsVisible="True">
    
        <!--Для контекста данных установлен верхний пример данных и первый элемент из коллекции примеров данных ниже,
         и LayoutRoot содержит корневую сетку, где размещается все содержимое страницы-->
        <Grid x:Name="LayoutRoot" Background="Transparent" d:DataContext="{Binding Items[0]}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
    
            <!--TitlePanel содержит имя приложения и заголовок страницы-->
            <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
                <TextBlock Text="МОЕ ПРИЛОЖЕНИЕ" Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBlock Text="{Binding LineOne}" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
            </StackPanel>
    
            <!--ContentPanel содержит текст сведений. Поместите здесь дополнительное содержимое-->
            <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
                <TextBlock Text="{Binding LineThree}" TextWrapping="Wrap" Style="{StaticResource PhoneTextNormalStyle}"/>
            </Grid>
        </Grid>
    
    </phone:PhoneApplicationPage>
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Navigation;
    using Microsoft.Phone.Controls;
    using Microsoft.Phone.Shell;
    using L5_1.Resources;
    using L5_1.ViewModels;
    
    namespace L5_1
    {
        public partial class MainPage : PhoneApplicationPage
        {
            // Конструктор
            public MainPage()
            {
                InitializeComponent();
    
                // Задайте для контекста данных элемента управления LongListSelector пример данных
                DataContext = App.ViewModel;
    
            }
    
            // Загрузка данных для элементов ViewModel
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                if (!App.ViewModel.IsDataLoaded)
                {
                    App.ViewModel.LoadData();
                }
            }
    
            // Обработка выбранных элементов, измененных в LongListSelector
            private void MainLongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // Если выбранный элемент равен NULL (ничего не выбрано), никакие действия не требуются
                if (MainLongListSelector.SelectedItem == null)
                    return;
    
                // Переход на новую страницу
                NavigationService.Navigate(new Uri("/DetailsPage.xaml?selectedItem=" +
                 (MainLongListSelector.SelectedItem as ItemViewModel).ID, UriKind.Relative));
    
                // Сброс выбранного элемента в NULL (ничего не выбрано)
                MainLongListSelector.SelectedItem = null;
            }
      
        }
    }
    
    using System;
    using System.Diagnostics;
    using System.Resources;
    using System.Windows;
    using System.Windows.Markup;
    using System.Windows.Navigation;
    using Microsoft.Phone.Controls;
    using Microsoft.Phone.Shell;
    using L5_1.Resources;
    using L5_1.ViewModels;
    
    namespace L5_1
    {
        public partial class App : Application
        {
            private static MainViewModel viewModel = null;
    
            /// <summary>
            /// Статический элемент ViewModel, используемый в представлениях для привязки.
            /// </summary>
            /// <returns>Объект MainViewModel.</returns>
            public static MainViewModel ViewModel
            {
                get
                {
                    // Отложить создание модели представления до необходимости
                    if (viewModel == null)
                        viewModel = new MainViewModel();
    
                    return viewModel;
                }
            }
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Navigation;
    using Microsoft.Phone.Controls;
    using Microsoft.Phone.Shell;
    using L5_1.Resources;
    
    namespace L5_1
    {
        public partial class DetailsPage : PhoneApplicationPage
        {
            // Конструктор
            public DetailsPage()
            {
                InitializeComponent();
    
            }
    
            // При переходе на страницу установите для контекста данных выбранный элемент в списке
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                if (DataContext == null)
                {
                    string selectedIndex = "";
                    if (NavigationContext.QueryString.TryGetValue("selectedItem", out selectedIndex))
                    {
                        int index = int.Parse(selectedIndex);
                        DataContext = App.ViewModel.Items[index];
                    }
                }
            }
        }
    }
    

    Если говорить о классе ItemViewModel (Листинг 21.7), то он не связан с представлениями, он реализует интерфейс INotifyPropertyChanged, событие PropertyChanged вызывается при изменении свойств, которые описаны в классе.

    using System;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    
    namespace L5_1.ViewModels
    {
        public class ItemViewModel : INotifyPropertyChanged
        {
            private string _id;
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется для идентификации объекта.
            /// </summary>
            /// <returns></returns>
            public string ID
            {
                get
                {
                    return _id;
                }
                set
                {
                    if (value != _id)
                    {
                        _id = value;
                        NotifyPropertyChanged("ID");
                    }
                }
            }
    
            private string _lineOne;
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется в представлении для отображения его значения с помощью привязки.
            /// </summary>
            /// <returns></returns>
            public string LineOne
            {
                get
                {
                    return _lineOne;
                }
                set
                {
                    if (value != _lineOne)
                    {
                        _lineOne = value;
                        NotifyPropertyChanged("LineOne");
                    }
                }
            }
    
            private string _lineTwo;
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется в представлении для отображения его значения с помощью привязки.
            /// </summary>
            /// <returns></returns>
            public string LineTwo
            {
                get
                {
                    return _lineTwo;
                }
                set
                {
                    if (value != _lineTwo)
                    {
                        _lineTwo = value;
                        NotifyPropertyChanged("LineTwo");
                    }
                }
            }
    
            private string _lineThree;
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется в представлении для отображения его значения с помощью привязки.
            /// </summary>
            /// <returns></returns>
            public string LineThree
            {
                get
                {
                    return _lineThree;
                }
                set
                {
                    if (value != _lineThree)
                    {
                        _lineThree = value;
                        NotifyPropertyChanged("LineThree");
                    }
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
            private void NotifyPropertyChanged(String propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (null != handler)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }
    

    В классе MainViewModel так же реализован INotifyPropertyChanged. Здесь событие PropertyChanged генерируется при изменении свойства SampleProperty. При этом класс содержит открытую коллекцию Items типа ItemViewModel – именно элементы этой коллекции, которая заполняется в этом же классе, и представляют собой те данные, которые при выполнении программы выводятся на главной странице в виде списка, и она же поставляет данные для страницы просмотра подробных сведений об элементе.

    using System;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using L5_1.Resources;
    
    namespace L5_1.ViewModels
    {
        public class MainViewModel : INotifyPropertyChanged
        {
            public MainViewModel()
            {
                this.Items = new ObservableCollection<ItemViewModel>();
            }
    
            /// <summary>
            /// Коллекция объектов ItemViewModel.
            /// </summary>
            public ObservableCollection<ItemViewModel> Items { get; private set; }
    
            private string _sampleProperty = "Sample Runtime Property Value";
            /// <summary>
            /// Пример свойства ViewModel; это свойство используется в представлении 
    для отображения его значения с помощью привязки
            /// </summary>
            /// <returns></returns>
            public string SampleProperty
            {
                get
                {
                    return _sampleProperty;
                }
                set
                {
                    if (value != _sampleProperty)
                    {
                        _sampleProperty = value;
                        NotifyPropertyChanged("SampleProperty");
                    }
                }
            }
    
            /// <summary>
            /// Пример свойства, возвращающего локализованную строку
            /// </summary>
            public string LocalizedSampleProperty
            {
                get
                {
                    return AppResources.SampleProperty;
                }
            }
    
            public bool IsDataLoaded
            {
                get;
                private set;
            }
    
            /// <summary>
            /// Создает и добавляет несколько объектов ItemViewModel в коллекцию элементов.
            /// </summary>
            public void LoadData()
            {
                // Пример данных; замените реальными данными
                this.Items.Add(new ItemViewModel() { ID = "0", LineOne = "runtime one", 
                LineTwo = "Maecenas praesent accumsan bibendum", 
                LineThree = "Facilisi faucibus habitant inceptos interdum lobortis 
    nascetur pharetra placerat pulvinar sagittis senectus sociosqu" });
                this.Items.Add(new ItemViewModel() { ID = "1", LineOne = "runtime two", 
                LineTwo = "Dictumst eleifend facilisi faucibus", 
                LineThree = "Suscipit torquent ultrices vehicula volutpat maecenas praesent accumsan
     bibendum dictumst eleifend facilisi faucibus" });
                this.IsDataLoaded = true;
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
            private void NotifyPropertyChanged(String propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (null != handler)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }
    

    В результате использования в приложении, построенном по шаблону Приложение с привязкой данных, шаблона MVVM, мы, по сравнению с приложением, построенном, например, по шаблону Приложение для Windows Phone получаем некоторые преимущества. А именно – код – работающий с данными и презентационная логика приложения разделены. При создании интерфейса приложения мы получаем возможность работать с данными периода проектирования, которые позволяют при проектировании отобразить интерфейс, основанный на шаблоне (в частности, на странице MainPage) в виде, максимально приближенном к виду работающего приложения. И хотя этот шаблон не вполне соответствует принципам MVVM, использование подходов, примененных в нём, в проектах, которые предусматривают работу с некоторыми наборами данных значительно облегчает создание подобных проектов. Кроме того, освоение этого проекта позволяет переходить к освоению других инструментов и примеров приложений.

    Нужно отметить, что приложения, построенные по шаблонам Приложение Windows Phone с панорамой и Приложение Windows Phone Pivot используют подход, похожий на тот, который применяется в шаблоне Приложение Windows Phone с привязкой к данным.

    Материалы для дальнейшего изучения

    В качестве наглядной иллюстрации реализации концепции MVVM в приложениях для Windows Phone, можно привести пример "Model-View-ViewModel" (http://code.msdn.microsoft.com/wpapps/Model-View-ViewModel-Sample-8cb92fd9). Его можно либо загрузить по вышеуказанной ссылке, либо, в Visual Studio, в окне Создать проект перейти в группу В Интернете > Примеры. Здесь, в строке поиска, нужно ввести MVVM, что приведет к нахождению нескольких примеров, среди которых нас интересует "Model-View-ViewModel" (рис. 21.2), в данном случае – для Visual C#, тот же пример доступен и для Visual Basic.

    (рис 21.2) Результаты поиска по примерам в окне Создать проект

    Пример можно выбрать и создать с его помощью проект. Кроме того, для ознакомления с данным примером можно воспользоваться пошаговым руководством "Реализация шаблона MVVM в приложении для Windows Phone" (http://msdn.microsoft.com/en-us/library/windowsphone/develop/gg521153%28v=vs.105%29.aspx).

    Для построения более совершенных, с точки зрения реализации MVVM, проектов, можно воспользоваться специальными наборами инструментов. Например, MVVM Light Toolkit (http://mvvmlight.codeplex.com/releases). На указанной странице проекта можно загрузить его установочный комплект, после установки которого и установки соответствующего VSIX-файла (инструкции по установке отображаются после инсталляции пакета) в списке C#-шаблонов приложений для Windows Phone появляется новый шаблон. MVVM Light доступен и через службу NuGet (Сервис > Диспетчер библиотек > Управление пакетами NuGet для решения).

    Существуют и другие наборы инструментов, облегчающие и ускоряющие разработку с использованием шаблона проектирования MVVM.

    Выводы

    В этой лекции мы рассмотрели основные понятия шаблона проектирования приложений MVVM, ознакомились с понятиями модели, представления, модели представления, привязки данных и команд. Применение данного шаблона позволяет упростить разработку сложных программных проектов за счёт чёткого разделения подсистем работы с данными и подсистем пользовательского интерфейса.

    Задание

    Проанализируйте идею приложения, которое вы разрабатываете, с точки зрения применения при его разработке шаблона проектирования MVVM. Разбейте логику приложения на модель, представление и модель представления, подготовьте отчёт о проделанной работе.

    Дополнительные материалы

    К данной лекции подготовлено видеоприложение и демонстрационный программный проект.

    Вернуться к учебному плану